Skip to content

Commit dae29b4

Browse files
committed
fix: push witt cwd
1 parent d3abe62 commit dae29b4

2 files changed

Lines changed: 149 additions & 2 deletions

File tree

internal/core/services/registry/oci.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"net/http"
78
"os"
89
"path/filepath"
910
"strings"
@@ -24,6 +25,11 @@ import (
2425

2526
type OciClient struct {
2627
credentialStore *CredentialStore
28+
// httpClient optionally overrides the HTTP client used by GetRepo.
29+
// When nil, the default retry client or auth client is used.
30+
httpClient *http.Client
31+
// plainHTTP forces plain HTTP (no TLS) for registry communication.
32+
plainHTTP bool
2733
}
2834

2935
func NewOciClient(credentialStore *CredentialStore) *OciClient {
@@ -38,13 +44,26 @@ func (c *OciClient) GetRepo(repoUrl string) (*remote.Repository, error) {
3844
return nil, err
3945
}
4046

47+
repo.PlainHTTP = c.plainHTTP
48+
49+
httpClient := retry.DefaultClient
50+
if c.httpClient != nil {
51+
httpClient = c.httpClient
52+
}
53+
4154
cred, _ := c.credentialStore.CredentialForRepo(repoUrl)
4255
if cred.Username == "" || cred.Password == "" {
4356
logger.Log().Warn("No registry credentials found for " + repoUrl + ". Trying to pull anonymously")
57+
if c.httpClient != nil {
58+
repo.Client = &auth.Client{
59+
Client: httpClient,
60+
Cache: auth.DefaultCache,
61+
}
62+
}
4463
} else {
4564
host := extractHost(repoUrl)
4665
repo.Client = &auth.Client{
47-
Client: retry.DefaultClient,
66+
Client: httpClient,
4867
Cache: auth.DefaultCache,
4968
Credential: auth.StaticCredential(host, cred),
5069
}
@@ -311,9 +330,22 @@ func (c *OciClient) Push(folder string, repo string, tag string, overrides map[s
311330
logger.Log().Warn(fmt.Sprintf("data chunk path %s does not exist, skipping", chunk.Path))
312331
continue
313332
}
333+
fileInfo, err := os.Stat(chunkFullPath)
334+
if err != nil {
335+
return v1.Descriptor{}, fmt.Errorf("failed to stat data chunk %s: %w", chunk.Name, err)
336+
}
337+
// Some registries reject zero-byte blob uploads (sha256:e3b0...).
338+
// Skip empty data files to keep push resilient.
339+
if fileInfo.Mode().IsRegular() && fileInfo.Size() == 0 {
340+
logger.Log().Warn(fmt.Sprintf("data chunk %s is empty, skipping", chunk.Path))
341+
continue
342+
}
314343
// Name the layer "data/<path>" so it extracts to the correct location on pull
315344
layerName := filepath.Join("data", chunk.Path)
316-
desc, err := fs.Add(context.Background(), layerName, string(domain.ArtifactTypeScrollData), chunkFullPath)
345+
// Use a path relative to the file store root (folder), not the full chunkFullPath,
346+
// because fs.Add resolves relative paths against its working directory.
347+
chunkStoreRelPath := filepath.Join("data", chunk.Path)
348+
desc, err := fs.Add(context.Background(), layerName, string(domain.ArtifactTypeScrollData), chunkStoreRelPath)
317349
if err != nil {
318350
return v1.Descriptor{}, fmt.Errorf("failed to pack data chunk %s: %w", chunk.Name, err)
319351
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package registry
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/http/httptest"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
"testing"
11+
12+
"github.com/highcard-dev/daemon/internal/core/domain"
13+
)
14+
15+
// fakeRegistry returns a plain-HTTP httptest server that implements the bare
16+
// minimum of the OCI Distribution spec so that oras.Copy can complete a push.
17+
func fakeRegistry(t *testing.T) *httptest.Server {
18+
t.Helper()
19+
blobs := map[string][]byte{}
20+
mux := http.NewServeMux()
21+
22+
mux.HandleFunc("GET /v2/", func(w http.ResponseWriter, r *http.Request) {
23+
w.WriteHeader(http.StatusOK)
24+
})
25+
26+
mux.HandleFunc("HEAD /v2/{rest...}", func(w http.ResponseWriter, r *http.Request) {
27+
parts := strings.Split(r.URL.Path, "/blobs/")
28+
if len(parts) == 2 {
29+
if _, ok := blobs[parts[1]]; ok {
30+
w.WriteHeader(http.StatusOK)
31+
return
32+
}
33+
}
34+
w.WriteHeader(http.StatusNotFound)
35+
})
36+
37+
mux.HandleFunc("POST /v2/{rest...}", func(w http.ResponseWriter, r *http.Request) {
38+
w.Header().Set("Location", fmt.Sprintf("%s?upload=1", r.URL.Path))
39+
w.WriteHeader(http.StatusAccepted)
40+
})
41+
42+
mux.HandleFunc("PUT /v2/{rest...}", func(w http.ResponseWriter, r *http.Request) {
43+
digest := r.URL.Query().Get("digest")
44+
if digest != "" {
45+
body := make([]byte, r.ContentLength)
46+
r.Body.Read(body)
47+
blobs[digest] = body
48+
w.Header().Set("Docker-Content-Digest", digest)
49+
w.WriteHeader(http.StatusCreated)
50+
return
51+
}
52+
body := make([]byte, r.ContentLength)
53+
r.Body.Read(body)
54+
w.WriteHeader(http.StatusCreated)
55+
})
56+
57+
srv := httptest.NewServer(mux)
58+
t.Cleanup(srv.Close)
59+
return srv
60+
}
61+
62+
// TestPushDataChunkPathNotDoubled calls OciClient.Push directly with a
63+
// relative scroll folder containing a data directory, pushing to a fake
64+
// in-process OCI registry. This verifies the data-chunk file paths are
65+
// resolved correctly (store-relative) and do not get doubled.
66+
//
67+
// Regression test for: when --cwd is a relative path like
68+
// ./scrolls/minecraft/1.17, the ORAS file store root is resolved to an
69+
// absolute path internally. Passing the full relative chunkFullPath
70+
// (scrolls/minecraft/1.17/data/<file>) to fs.Add caused the store to look
71+
// for <abs-root>/scrolls/minecraft/1.17/data/<file> which doesn't exist.
72+
func TestPushDataChunkPathNotDoubled(t *testing.T) {
73+
tmpDir := t.TempDir()
74+
t.Chdir(tmpDir)
75+
76+
srv := fakeRegistry(t)
77+
// httptest URL is http://127.0.0.1:<port>; strip scheme for oras.
78+
registryHost := strings.TrimPrefix(srv.URL, "http://")
79+
80+
// Build a minimal scroll directory tree via a relative path.
81+
relFolder := filepath.Join("scrolls", "minecraft", "1.17")
82+
if err := os.MkdirAll(relFolder, 0755); err != nil {
83+
t.Fatal(err)
84+
}
85+
if err := os.WriteFile(
86+
filepath.Join(relFolder, "scroll.yaml"),
87+
[]byte("name: test\nversion: 0.1.0\napp_version: \"1.17\"\n"),
88+
0644,
89+
); err != nil {
90+
t.Fatal(err)
91+
}
92+
93+
// Create a data directory with a file – this is the path that was doubled.
94+
dataDir := filepath.Join(relFolder, "data")
95+
if err := os.MkdirAll(dataDir, 0755); err != nil {
96+
t.Fatal(err)
97+
}
98+
if err := os.WriteFile(filepath.Join(dataDir, "server.properties"), []byte("motd=test\n"), 0644); err != nil {
99+
t.Fatal(err)
100+
}
101+
102+
// Create an OciClient configured for plain HTTP pointing at our fake registry.
103+
credStore := NewCredentialStore([]domain.RegistryCredential{})
104+
client := &OciClient{
105+
credentialStore: credStore,
106+
plainHTTP: true,
107+
}
108+
109+
repoRef := registryHost + "/test/scroll"
110+
111+
_, err := client.Push(relFolder, repoRef, "1.17", map[string]string{}, false, nil)
112+
if err != nil {
113+
t.Fatalf("Push failed unexpectedly: %v", err)
114+
}
115+
}

0 commit comments

Comments
 (0)