Skip to content

Commit 6b511ab

Browse files
committed
ipfs: add support to pull images by ref
Signed-off-by: abushwang <[email protected]>
1 parent d5f314b commit 6b511ab

File tree

8 files changed

+539
-20
lines changed

8 files changed

+539
-20
lines changed

cmd/go.mod

+5-1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ require (
4949
github.com/containernetworking/plugins v1.5.1 // indirect
5050
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
5151
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
52+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
5253
github.com/distribution/reference v0.6.0 // indirect
5354
github.com/docker/cli v27.5.1+incompatible // indirect
5455
github.com/docker/docker-credential-helpers v0.7.0 // indirect
@@ -75,10 +76,12 @@ require (
7576
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
7677
github.com/imdario/mergo v0.3.13 // indirect
7778
github.com/intel/goresctrl v0.8.0 // indirect
78-
github.com/ipfs/go-cid v0.1.0 // indirect
79+
github.com/ipfs/go-cid v0.4.1 // indirect
7980
github.com/josharian/intern v1.0.0 // indirect
8081
github.com/json-iterator/go v1.1.12 // indirect
8182
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
83+
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
84+
github.com/libp2p/go-libp2p v0.32.2 // indirect
8285
github.com/mailru/easyjson v0.7.7 // indirect
8386
github.com/mdlayher/socket v0.4.1 // indirect
8487
github.com/mdlayher/vsock v1.2.1 // indirect
@@ -98,6 +101,7 @@ require (
98101
github.com/multiformats/go-base36 v0.2.0 // indirect
99102
github.com/multiformats/go-multiaddr v0.14.0 // indirect
100103
github.com/multiformats/go-multibase v0.2.0 // indirect
104+
github.com/multiformats/go-multicodec v0.9.0 // indirect
101105
github.com/multiformats/go-multihash v0.2.3 // indirect
102106
github.com/multiformats/go-varint v0.0.7 // indirect
103107
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect

ipfs/client/client.go

+242
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package client
1818

1919
import (
20+
"bytes"
2021
"encoding/json"
2122
"fmt"
2223
"io"
@@ -27,6 +28,7 @@ import (
2728
"path/filepath"
2829
"strings"
2930

31+
"github.com/containerd/stargz-snapshotter/ipfs/ipnskey"
3032
"github.com/mitchellh/go-homedir"
3133
ma "github.com/multiformats/go-multiaddr"
3234
manet "github.com/multiformats/go-multiaddr/net"
@@ -226,3 +228,243 @@ func GetIPFSAPIAddress(ipfsPath string, scheme string) (string, error) {
226228
}
227229
return iurl, nil
228230
}
231+
232+
// Resolve resolves the IPNS name to its corresponding CID.
233+
func (c *Client) Resolve(ref string) (string, error) {
234+
if c.Address == "" {
235+
return "", fmt.Errorf("specify IPFS API address")
236+
}
237+
238+
kg, err := ipnskey.GetKeyGen(ref)
239+
if err != nil {
240+
return "", fmt.Errorf("failed to get key gen: %w", err)
241+
}
242+
243+
peerID := kg.GetKeyID()
244+
245+
client := c.Client
246+
if client == nil {
247+
client = http.DefaultClient
248+
}
249+
250+
ipfsAPINameResolve := c.Address + "/api/v0/name/resolve"
251+
req, err := http.NewRequest("POST", ipfsAPINameResolve, nil)
252+
if err != nil {
253+
return "", err
254+
}
255+
256+
q := req.URL.Query()
257+
q.Add("arg", "/ipns/"+peerID)
258+
q.Add("nocache", "true")
259+
req.URL.RawQuery = q.Encode()
260+
261+
resp, err := client.Do(req)
262+
if err != nil {
263+
return "", err
264+
}
265+
defer func() {
266+
io.Copy(io.Discard, resp.Body)
267+
resp.Body.Close()
268+
}()
269+
270+
if resp.StatusCode/100 != 2 {
271+
return "", fmt.Errorf("failed to resolve name %v; status code: %v", peerID, resp.StatusCode)
272+
}
273+
274+
var rs struct {
275+
Path string `json:"Path"`
276+
}
277+
if err := json.NewDecoder(resp.Body).Decode(&rs); err != nil {
278+
return "", err
279+
}
280+
281+
parts := strings.Split(rs.Path, "/")
282+
if len(parts) < 3 || parts[1] != "ipfs" {
283+
return "", fmt.Errorf("invalid resolved path format: %s", rs.Path)
284+
}
285+
286+
return parts[2], nil
287+
}
288+
289+
// Publish publishes the given CID to IPNS using the key associated with the given ref.
290+
func (c *Client) Publish(ref string, cid string) error {
291+
if c.Address == "" {
292+
return fmt.Errorf("specify IPFS API address")
293+
}
294+
295+
if err := c.importKey(ref); err != nil {
296+
return fmt.Errorf("failed to import key: %w", err)
297+
}
298+
299+
client := c.Client
300+
if client == nil {
301+
client = http.DefaultClient
302+
}
303+
304+
ipfsAPINamePublish := c.Address + "/api/v0/name/publish"
305+
req, err := http.NewRequest("POST", ipfsAPINamePublish, nil)
306+
if err != nil {
307+
return err
308+
}
309+
310+
q := req.URL.Query()
311+
q.Add("arg", "/ipfs/"+cid)
312+
q.Add("key", ref)
313+
314+
q.Add("allow-offline", "true")
315+
316+
req.URL.RawQuery = q.Encode()
317+
318+
resp, err := client.Do(req)
319+
if err != nil {
320+
return err
321+
}
322+
defer func() {
323+
io.Copy(io.Discard, resp.Body)
324+
resp.Body.Close()
325+
}()
326+
327+
respBody, err := io.ReadAll(resp.Body)
328+
if err != nil {
329+
return fmt.Errorf("failed to read response body: %v", err)
330+
}
331+
332+
if resp.StatusCode/100 != 2 {
333+
return fmt.Errorf("failed to publish; status code: %v, body: %s\n"+
334+
"Request URL: %s", resp.StatusCode, string(respBody), ipfsAPINamePublish)
335+
}
336+
337+
return nil
338+
}
339+
340+
// importKey imports the key pair associated with the given ref into the local IPFS node.
341+
// The ref will be used as the key name in IPFS. If the key already exists, it will return nil.
342+
func (c *Client) importKey(ref string) error {
343+
if c.Address == "" {
344+
return fmt.Errorf("specify IPFS API address")
345+
}
346+
347+
exists, err := c.checkKeyExists(ref)
348+
if err != nil {
349+
return fmt.Errorf("failed to check key existence: %w", err)
350+
}
351+
if exists {
352+
return nil
353+
}
354+
355+
kg, err := ipnskey.GetKeyGen(ref)
356+
if err != nil {
357+
return fmt.Errorf("failed to get key gen: %w", err)
358+
}
359+
360+
keyData := kg.GetKeyData()
361+
362+
body := &bytes.Buffer{}
363+
writer := multipart.NewWriter(body)
364+
365+
safeFilename := strings.ReplaceAll(ref, "/", "_")
366+
safeFilename = strings.ReplaceAll(safeFilename, ":", "_")
367+
368+
part, err := writer.CreateFormFile("file", safeFilename+".pem")
369+
if err != nil {
370+
return fmt.Errorf("failed to create form file: %v", err)
371+
}
372+
373+
_, err = part.Write(keyData)
374+
if err != nil {
375+
return fmt.Errorf("failed to write key data: %v", err)
376+
}
377+
378+
err = writer.Close()
379+
if err != nil {
380+
return fmt.Errorf("failed to close multipart writer: %v", err)
381+
}
382+
383+
encodedKeyname := url.QueryEscape(ref)
384+
ipfsAPIKeyImport := fmt.Sprintf("%s/api/v0/key/import?arg=%s&format=pem-pkcs8-cleartext", c.Address, encodedKeyname)
385+
386+
req, err := http.NewRequest("POST", ipfsAPIKeyImport, body)
387+
if err != nil {
388+
return fmt.Errorf("failed to create HTTP request: %v", err)
389+
}
390+
391+
req.Header.Set("Content-Type", writer.FormDataContentType())
392+
393+
client := c.Client
394+
if client == nil {
395+
client = http.DefaultClient
396+
}
397+
398+
resp, err := client.Do(req)
399+
if err != nil {
400+
return fmt.Errorf("failed to send request: %v", err)
401+
}
402+
defer resp.Body.Close()
403+
404+
respBody, err := io.ReadAll(resp.Body)
405+
if err != nil {
406+
return fmt.Errorf("failed to read response body: %v", err)
407+
}
408+
409+
if resp.StatusCode != http.StatusOK {
410+
return fmt.Errorf("IPFS API returned error status: %d, body: %s\nRequest URL: %s", resp.StatusCode, string(respBody), ipfsAPIKeyImport)
411+
}
412+
413+
return nil
414+
}
415+
416+
// checkKeyExists checks if a key with the given name already exists in IPFS
417+
func (c *Client) checkKeyExists(name string) (bool, error) {
418+
client := c.Client
419+
if client == nil {
420+
client = http.DefaultClient
421+
}
422+
423+
ipfsAPIKeyList := c.Address + "/api/v0/key/list"
424+
req, err := http.NewRequest("POST", ipfsAPIKeyList, nil)
425+
if err != nil {
426+
return false, err
427+
}
428+
429+
resp, err := client.Do(req)
430+
if err != nil {
431+
return false, err
432+
}
433+
defer func() {
434+
io.Copy(io.Discard, resp.Body)
435+
resp.Body.Close()
436+
}()
437+
438+
if resp.StatusCode/100 != 2 {
439+
return false, fmt.Errorf("failed to list keys; status code: %v", resp.StatusCode)
440+
}
441+
442+
var result struct {
443+
Keys []struct {
444+
Name string `json:"Name"`
445+
} `json:"Keys"`
446+
}
447+
448+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
449+
return false, err
450+
}
451+
452+
for _, key := range result.Keys {
453+
if key.Name == name {
454+
return true, nil
455+
}
456+
}
457+
458+
return false, nil
459+
}
460+
461+
func (c *Client) IsRef(s string) bool {
462+
parts := strings.Split(s, "/")
463+
lastPart := parts[len(parts)-1]
464+
465+
if strings.Contains(lastPart, ":") || strings.Contains(lastPart, "@") {
466+
return true
467+
}
468+
469+
return len(parts) >= 2
470+
}

ipfs/client/client_test.go

+61
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ package client
2121
import (
2222
"bytes"
2323
"flag"
24+
"fmt"
2425
"io"
26+
"net/http"
2527
"testing"
2628
)
2729

@@ -48,6 +50,7 @@ func TestIPFSClient(t *testing.T) {
4850
}
4951
checkData(t, c, cid, 0, len(sampleString), sampleString, len(sampleString))
5052
checkData(t, c, cid, 10, 4, sampleString[10:14], len(sampleString))
53+
testPublishAndResolve(t, c, cid)
5154
}
5255

5356
func checkData(t *testing.T, c *Client, cid string, off, len int, wantData string, allSize int) {
@@ -75,3 +78,61 @@ func checkData(t *testing.T, c *Client, cid string, off, len int, wantData strin
7578
return
7679
}
7780
}
81+
82+
func testPublishAndResolve(t *testing.T, c *Client, cid string) {
83+
ref := "test/ref:example"
84+
85+
if err := c.Publish(ref, cid); err != nil {
86+
t.Errorf("failed to publish CID: %v", err)
87+
return
88+
}
89+
90+
resolvedCID, err := c.Resolve(ref)
91+
if err != nil {
92+
t.Errorf("failed to resolve ref: %v", err)
93+
return
94+
}
95+
96+
if resolvedCID != cid {
97+
t.Errorf("unexpected resolved CID: got %v, want %v", resolvedCID, cid)
98+
}
99+
100+
// Clean up the imported key
101+
if err := c.removeKey(ref); err != nil {
102+
t.Errorf("failed to remove key: %v", err)
103+
}
104+
}
105+
106+
// removeKey removes the key associated with the given ref
107+
func (c *Client) removeKey(ref string) error {
108+
if c.Address == "" {
109+
return fmt.Errorf("specify IPFS API address")
110+
}
111+
112+
client := c.Client
113+
if client == nil {
114+
client = http.DefaultClient
115+
}
116+
117+
ipfsAPIKeyRemove := c.Address + "/api/v0/key/rm"
118+
req, err := http.NewRequest("POST", ipfsAPIKeyRemove, nil)
119+
if err != nil {
120+
return err
121+
}
122+
123+
q := req.URL.Query()
124+
q.Add("arg", ref)
125+
req.URL.RawQuery = q.Encode()
126+
127+
resp, err := client.Do(req)
128+
if err != nil {
129+
return err
130+
}
131+
defer resp.Body.Close()
132+
133+
if resp.StatusCode != http.StatusOK {
134+
return fmt.Errorf("failed to remove key; status code: %v", resp.StatusCode)
135+
}
136+
137+
return nil
138+
}

ipfs/converter.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,16 @@ func PushWithIPFSPath(ctx context.Context, client *containerd.Client, ref string
7070
if err != nil {
7171
return "", err
7272
}
73-
return iclient.Add(bytes.NewReader(root))
73+
cid, err := iclient.Add(bytes.NewReader(root))
74+
if err != nil {
75+
return "", err
76+
}
77+
if iclient.IsRef(ref) {
78+
if err := iclient.Publish(ref, cid); err != nil {
79+
return "", err
80+
}
81+
}
82+
return cid, nil
7483
}
7584

7685
func pushBlobHook(client *ipfsclient.Client) converter.ConvertHookFunc {

0 commit comments

Comments
 (0)