diff --git a/.dagger/checks.go b/.dagger/checks.go index 145c5f6..23ed884 100644 --- a/.dagger/checks.go +++ b/.dagger/checks.go @@ -23,8 +23,10 @@ func (m *BargeDev) TestsPass( ctx context.Context, // +optional githubToken *dagger.Secret, + // +optional + githubRepo string, ) error { - if _, err := m.Test(ctx, githubToken); err != nil { + if _, err := m.Test(ctx, githubToken, githubRepo); err != nil { return err } diff --git a/.dagger/main.go b/.dagger/main.go index 2c738b9..10754e5 100644 --- a/.dagger/main.go +++ b/.dagger/main.go @@ -62,6 +62,8 @@ func (m *BargeDev) Test( ctx context.Context, // +optional githubToken *dagger.Secret, + // +optional + githubRepo string, ) (string, error) { return dag.Go(dagger.GoOpts{ Module: m.Source, @@ -70,9 +72,13 @@ func (m *BargeDev) Test( Container(). With(func(r *dagger.Container) *dagger.Container { if githubToken != nil { - return r. + r = r. WithSecretVariable("GITHUB_TOKEN", githubToken) } + if githubRepo != "" { + r = r. + WithEnvVariable("GITHUB_REPOSITORY", githubRepo) + } return r }). WithExec([]string{"go", "test", "-race", "-cover", "-test.v", "./..."}, dagger.ContainerWithExecOpts{ExperimentalPrivilegedNesting: true}). diff --git a/.env b/.env index 94406a3..da8d53e 100644 --- a/.env +++ b/.env @@ -1,4 +1,6 @@ bargeDev_test_githubToken=env://GITHUB_TOKEN +bargeDev_test_githubRepo=frantjc/barge bargeDev_testsPass_githubToken=env://GITHUB_TOKEN +bargeDev_testsPass_githubRepo=frantjc/barge bargeDev_release_githubToken=env://GITHUB_TOKEN bargeDev_release_githubRepo=frantjc/barge diff --git a/README.md b/README.md index b625350..bb2c678 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,21 @@ barge cp https://github.com/frantjc/barge/raw/refs/heads/main/testdata/test-0.1. ```sh barge cp oci://ghcr.io/frantjc/barge/charts/test ./test.tgz ``` + +- You want to do all of the above at once where `barge-sync.yml` looks like: + +```yml +--- +sources: + - url: repo://chartmuseum + charts: + chartmuseum: + - 3.9.0 + chartsBySemver: + chartmuseum: ">= 3.10" +``` + +```sh +helm repo add chartmuseum https://chartmuseum.github.io/charts +barge sync barge-sync.yml oci://example.io +``` diff --git a/cmd/barge/main.go b/cmd/barge/main.go index fcb3514..d7eb2bd 100644 --- a/cmd/barge/main.go +++ b/cmd/barge/main.go @@ -44,7 +44,7 @@ func newBarge() *cobra.Command { Version: SemVer(), SilenceErrors: true, SilenceUsage: true, - PersistentPreRun: func(cmd *cobra.Command, args []string) { + PersistentPreRun: func(cmd *cobra.Command, _ []string) { cmd.SetContext( util.SloggerInto( util.StdoutInto( @@ -62,17 +62,17 @@ func newBarge() *cobra.Command { }, } ) - cmd.Flags().BoolP("help", "h", false, "Help for "+cmd.Name()) + cmd.PersistentFlags().BoolP("help", "h", false, "Help for "+cmd.Name()) cmd.Flags().Bool("version", false, "Version for "+cmd.Name()) cmd.SetVersionTemplate("{{ .Name }}{{ .Version }}") slogConfig.AddFlags(cmd.PersistentFlags()) - cmd.AddCommand(newCopy()) + cmd.AddCommand(newCopy(), newSync()) return cmd } func newCopy() *cobra.Command { cmd := &cobra.Command{ - Use: "copy", + Use: "copy src dest", Aliases: []string{"cp"}, SilenceErrors: true, SilenceUsage: true, @@ -84,3 +84,21 @@ func newCopy() *cobra.Command { barge.AddFlags(cmd.Flags()) return cmd } + +func newSync() *cobra.Command { + var ( + syncOpts = new(barge.SyncOpts) + cmd = &cobra.Command{ + Use: "sync config dest", + SilenceErrors: true, + SilenceUsage: true, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + return barge.Sync(cmd.Context(), args[0], args[1]) + }, + } + ) + cmd.Flags().BoolVar(&syncOpts.FailFast, "fail-fast", false, "Exit when the first source fails to sync") + barge.AddFlags(cmd.Flags()) + return cmd +} diff --git a/copy_test.go b/copy_test.go index 714d4f6..9211d8f 100644 --- a/copy_test.go +++ b/copy_test.go @@ -6,19 +6,16 @@ import ( "net/url" "os" "os/exec" - "strings" "testing" "dagger.io/dagger" "github.com/frantjc/barge" _ "github.com/frantjc/barge/internal/archive" - _ "github.com/frantjc/barge/internal/artifactory" _ "github.com/frantjc/barge/internal/chartmuseum" _ "github.com/frantjc/barge/internal/directory" _ "github.com/frantjc/barge/internal/file" _ "github.com/frantjc/barge/internal/http" _ "github.com/frantjc/barge/internal/oci" - _ "github.com/frantjc/barge/internal/release" _ "github.com/frantjc/barge/internal/repo" "github.com/frantjc/barge/internal/util" "github.com/frantjc/barge/testdata" @@ -35,16 +32,65 @@ func Command(t testing.TB, name string, arg ...string) *exec.Cmd { return cmd } +func Context(t testing.TB) context.Context { + t.Helper() + return util.StdoutInto(util.StderrInto(t.Context(), t.Output()), t.Output()) +} + +func Chartmuseum(t testing.TB, dag *dagger.Client) *url.URL { + t.Helper() + ctx := t.Context() + chartmuseum, err := dag.Container(). + From("ghcr.io/helm/chartmuseum:v0.16.3"). + WithExposedPort(8080). + WithEnvVariable("DEBUG", "1"). + WithEnvVariable("STORAGE", "local"). + WithEnvVariable("STORAGE_LOCAL_ROOTDIR", "/tmp"). + AsService(). + Start(ctx) + require.NoError(t, err) + t.Cleanup(func() { + _, err = chartmuseum.Stop(context.WithoutCancel(ctx)) + assert.NoError(t, err) + }) + rawChartmuseumURL, err := chartmuseum.Endpoint(ctx, dagger.ServiceEndpointOpts{Scheme: "chartmuseum"}) + require.NoError(t, err) + chartmuseumURL, err := url.Parse(rawChartmuseumURL) + chartmuseumURL.RawQuery = "insecure=1" + require.NoError(t, err) + + return chartmuseumURL +} + +func Registry(t testing.TB, dag *dagger.Client) *url.URL { + t.Helper() + ctx := t.Context() + registry, err := dag.Container(). + From("docker.io/distribution/distribution:3"). + WithExposedPort(5000). + AsService(). + Start(ctx) + require.NoError(t, err) + t.Cleanup(func() { + _, err = registry.Stop(context.WithoutCancel(ctx)) + assert.NoError(t, err) + }) + rawRegistryURL, err := registry.Endpoint(ctx, dagger.ServiceEndpointOpts{Scheme: "oci"}) + require.NoError(t, err) + registryURL, err := url.Parse(rawRegistryURL) + require.NoError(t, err) + + return registryURL +} + func FuzzCopy(f *testing.F) { - ctx := util.StdoutInto(util.StderrInto(f.Context(), f.Output()), f.Output()) + ctx := Context(f) tmp, err := os.CreateTemp(f.TempDir(), "test-0.1.0.tgz") require.NoError(f, err) _, err = tmp.Write(testdata.ChartArchive) require.NoError(f, err) - f.Cleanup(func() { - assert.NoError(f, tmp.Close()) - }) + require.NoError(f, tmp.Close()) chart, err := loader.LoadFile(tmp.Name()) require.NoError(f, err) archive := fmt.Sprintf("archive://%s", tmp.Name()) @@ -61,25 +107,8 @@ func FuzzCopy(f *testing.F) { assert.NoError(f, dag.Close()) }) - chartmuseum, err := dag.Container(). - From("ghcr.io/helm/chartmuseum:v0.16.3"). - WithExposedPort(8080). - WithEnvVariable("DEBUG", "1"). - WithEnvVariable("STORAGE", "local"). - WithEnvVariable("STORAGE_LOCAL_ROOTDIR", "/tmp"). - AsService(). - Start(ctx) - require.NoError(f, err) - f.Cleanup(func() { - _, err = chartmuseum.Stop(context.WithoutCancel(ctx)) - assert.NoError(f, err) - }) - rawChartmuseumURL, err := chartmuseum.Endpoint(ctx, dagger.ServiceEndpointOpts{Scheme: "chartmuseum"}) - require.NoError(f, err) - chartmuseumURL, err := url.Parse(rawChartmuseumURL) - chartmuseumURL.RawQuery = "insecure=1" - require.NoError(f, err) - f.Add(archive, chartmuseumURL.String()) + chartmuseumURL := Chartmuseum(f, dag) + require.NoError(f, barge.Copy(ctx, archive, chartmuseumURL.String())) if helm, err := exec.LookPath("helm"); assert.NoError(f, err) { repo := "chartmuseum" @@ -101,32 +130,17 @@ func FuzzCopy(f *testing.F) { } f.Add(httpURL.String(), f.TempDir()) - registry, err := dag.Container(). - From("docker.io/distribution/distribution:3"). - WithExposedPort(5000). - AsService(). - Start(ctx) - require.NoError(f, err) - f.Cleanup(func() { - _, err = registry.Stop(context.WithoutCancel(ctx)) - assert.NoError(f, err) - }) - rawRegistryURL, err := registry.Endpoint(ctx, dagger.ServiceEndpointOpts{Scheme: "oci"}) - require.NoError(f, err) - registryURL, err := url.Parse(rawRegistryURL) - require.NoError(f, err) - + registryURL := Registry(f, dag) oci := registryURL.JoinPath("test") f.Add(archive, oci.String()) f.Add(oci.String(), f.TempDir()) - ociWithTag := registryURL.JoinPath("test:tag") f.Add(archive, ociWithTag.String()) f.Add(ociWithTag.String(), f.TempDir()) } - if username, _, err := util.GetGitHubAuth(ctx); assert.NoError(f, err) { - ghcr := fmt.Sprintf("oci://ghcr.io/%s/barge/charts/%s", username, chart.Name()) + if githubRepository := os.Getenv("GITHUB_REPOSITORY"); githubRepository != "" { + ghcr := fmt.Sprintf("oci://ghcr.io/%s/charts/%s", githubRepository, chart.Name()) ghcrWithTag := fmt.Sprintf("%s:%s", ghcr, chart.Metadata.Version) f.Add(archive, ghcr) f.Add(ghcr, f.TempDir()) @@ -135,13 +149,6 @@ func FuzzCopy(f *testing.F) { } f.Fuzz(func(t *testing.T, src, dest string) { - if strings.HasPrefix(src, "repo://") { - if helm, err := exec.LookPath("helm"); assert.NoError(f, err) { - update := Command(t, helm, "repo", "update") - require.NoError(t, update.Run()) - } - } - require.NoError(t, barge.Copy(t.Context(), src, dest)) }) } diff --git a/destination.go b/destination.go index f7c652b..58724e7 100644 --- a/destination.go +++ b/destination.go @@ -29,3 +29,7 @@ func RegisterDestination(o Destination, scheme string, schemes ...string) { destMux[s] = o } } + +type SyncableDestination interface { + Sync(context.Context, *url.URL, *chart.Chart) error +} diff --git a/go.mod b/go.mod index c3155d7..6b6f8df 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.4 require ( dagger.io/dagger v0.19.10 + github.com/Masterminds/semver/v3 v3.4.0 github.com/cli/cli/v2 v2.83.2 github.com/fluxcd/pkg/auth v0.33.0 github.com/frantjc/x v0.0.0-20251203020658-a4e29ee5477f @@ -56,7 +57,6 @@ require ( github.com/BurntSushi/toml v1.5.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/alecthomas/chroma/v2 v2.19.0 // indirect diff --git a/internal/artifactory/destination.go b/internal/artifactory/destination.go index 3c1a03d..0f0572b 100644 --- a/internal/artifactory/destination.go +++ b/internal/artifactory/destination.go @@ -72,3 +72,7 @@ func (d *destination) Write(ctx context.Context, u *url.URL, c *chart.Chart) err return nil } + +func (d *destination) Sync(ctx context.Context, u *url.URL, c *chart.Chart) error { + return d.Write(ctx, u, c) +} diff --git a/internal/chartmuseum/destination.go b/internal/chartmuseum/destination.go index c45f229..787632b 100644 --- a/internal/chartmuseum/destination.go +++ b/internal/chartmuseum/destination.go @@ -88,3 +88,7 @@ func (d *destination) Write(ctx context.Context, u *url.URL, c *chart.Chart) err return nil } + +func (d *destination) Sync(ctx context.Context, u *url.URL, c *chart.Chart) error { + return d.Write(ctx, u, c) +} diff --git a/internal/directory/destination.go b/internal/directory/destination.go index 727806d..e0406e8 100644 --- a/internal/directory/destination.go +++ b/internal/directory/destination.go @@ -2,6 +2,7 @@ package directory import ( "context" + "fmt" "net/url" "path/filepath" @@ -23,3 +24,7 @@ type destination struct{} func (d *destination) Write(ctx context.Context, u *url.URL, c *chart.Chart) error { return util.WriteChartToDirectory(ctx, c, filepath.Join(u.Host, u.Path)) } + +func (d *destination) Sync(ctx context.Context, u *url.URL, c *chart.Chart) error { + return util.WriteChartToFile(c, filepath.Join(u.Host, u.Path, fmt.Sprintf("%s-%s.tgz", c.Name(), c.Metadata.Version))) +} diff --git a/internal/file/destination.go b/internal/file/destination.go index 7c4eca4..88d19cb 100644 --- a/internal/file/destination.go +++ b/internal/file/destination.go @@ -2,6 +2,7 @@ package file import ( "context" + "fmt" "net/url" "os" "path/filepath" @@ -33,3 +34,15 @@ func (d *destination) Write(ctx context.Context, u *url.URL, c *chart.Chart) err return util.WriteChartToFile(c, name) } + +func (d *destination) Sync(ctx context.Context, u *url.URL, c *chart.Chart) error { + name := filepath.Join(u.Host, u.Path) + + if fi, err := os.Stat(name); err != nil { + return err + } else if fi.IsDir() { + return d.Write(ctx, u.JoinPath(fmt.Sprintf("%s-%s.tgz", c.Name(), c.Metadata.Version)), c) + } + + return fmt.Errorf("cannot sync to a file; try a directory") +} diff --git a/internal/oci/destination.go b/internal/oci/destination.go index ab91b19..a0f9d19 100644 --- a/internal/oci/destination.go +++ b/internal/oci/destination.go @@ -4,6 +4,7 @@ import ( "context" "io" "net/url" + "strings" "github.com/frantjc/barge" "github.com/frantjc/barge/internal/util" @@ -45,3 +46,12 @@ func (d *destination) Write(ctx context.Context, u *url.URL, c *chart.Chart) err return nil } + +func (d *destination) Sync(ctx context.Context, u *url.URL, c *chart.Chart) error { + v := u.JoinPath() + v.Path, _, _ = strings.Cut(v.Path, ":") + q := v.Query() + q.Set("version", c.Metadata.Version) + v.RawQuery = q.Encode() + return d.Write(ctx, v.JoinPath(c.Name()), c) +} diff --git a/internal/oci/source.go b/internal/oci/source.go index 4e1a03b..2c3b6d9 100644 --- a/internal/oci/source.go +++ b/internal/oci/source.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "net/url" + "strings" "github.com/frantjc/barge" "github.com/frantjc/barge/internal/util" @@ -35,3 +36,14 @@ func (s *source) Open(ctx context.Context, u *url.URL) (*chart.Chart, error) { return loader.LoadArchive(bytes.NewReader(res.Chart.Data)) } + +func (s *source) Versions(ctx context.Context, u *url.URL, name string) ([]string, error) { + r, err := util.NewRegistryClientFromURL(ctx, u) + if err != nil { + return nil, err + } + + ref, _, _ := strings.Cut(util.RefFromURL(u.JoinPath(name)), ":") + + return r.Tags(ref) +} diff --git a/internal/repo/source.go b/internal/repo/source.go index b40fd50..b7b514d 100644 --- a/internal/repo/source.go +++ b/internal/repo/source.go @@ -106,3 +106,33 @@ func (s *source) Open(ctx context.Context, u *url.URL) (*chart.Chart, error) { return nil, fmt.Errorf("could not get chart from urls: %w", errs) } + +func (s *source) Versions(ctx context.Context, u *url.URL, name string) ([]string, error) { + settings := barge.HelmSettings() + + repos, err := repo.LoadFile(settings.RepositoryConfig) + if err != nil { + return nil, err + } + + entry := repos.Get(u.Host) + if entry == nil { + return nil, fmt.Errorf("unknown repo %s", u.Host) + } + + index, err := repo.LoadIndexFile( + filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(u.Host)), + ) + if err != nil { + return nil, err + } + + versions, ok := index.Entries[name] + if !ok { + return nil, fmt.Errorf("chart %s not found in repo %s", name, u.Host) + } + + return xslices.Map(versions, func(v *repo.ChartVersion, _ int) string { + return v.Version + }), nil +} diff --git a/internal/util/user.go b/internal/util/auth.go similarity index 100% rename from internal/util/user.go rename to internal/util/auth.go diff --git a/internal/util/registry.go b/internal/util/registry.go index ba29c2e..e59c942 100644 --- a/internal/util/registry.go +++ b/internal/util/registry.go @@ -20,8 +20,6 @@ import ( ) func GetGitHubAuth(ctx context.Context) (string, string, error) { - stdout := StdoutFrom(ctx) - cfg, err := factory.New("v0.0.0-unknown").Config() if err != nil { return "", "", err @@ -29,16 +27,18 @@ func GetGitHubAuth(ctx context.Context) (string, string, error) { authCfg := cfg.Authentication() - httpClient, err := api.NewHTTPClient(api.HTTPClientOptions{ - Config: authCfg, - Log: stdout, - }) - if err != nil { - return "", "", err - } - username, err := authCfg.ActiveUser("github.com") if err != nil { + stdout := StdoutFrom(ctx) + + httpClient, err := api.NewHTTPClient(api.HTTPClientOptions{ + Config: authCfg, + Log: stdout, + }) + if err != nil { + return "", "", err + } + var nerr error username, nerr = api.CurrentLoginName(api.NewClientFromHTTP(httpClient), "github.com") if nerr != nil { @@ -89,7 +89,11 @@ func RefFromURL(u *url.URL) string { if strings.Contains(u.Path, ":") { return ref } - return fmt.Sprintf("%s:latest", ref) + tag := u.Query().Get("version") + if tag == "" { + tag = "latest" + } + return fmt.Sprintf("%s:%s", ref, tag) } func cliOptForURLAndProvider(u *url.URL, provider string) registry.ClientOption { diff --git a/source.go b/source.go index a8b7244..e4bb04f 100644 --- a/source.go +++ b/source.go @@ -29,3 +29,7 @@ func RegisterSource(o Source, scheme string, schemes ...string) { srcMux[s] = o } } + +type QueryableSource interface { + Versions(context.Context, *url.URL, string) ([]string, error) +} diff --git a/sync.go b/sync.go new file mode 100644 index 0000000..f88b381 --- /dev/null +++ b/sync.go @@ -0,0 +1,239 @@ +package barge + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "os" + + "github.com/Masterminds/semver/v3" + "github.com/frantjc/barge/internal/util" + "golang.org/x/sync/errgroup" + "sigs.k8s.io/yaml" +) + +type URL url.URL + +func (j *URL) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + u, err := url.Parse(raw) + if err != nil { + return err + } + + *j = URL(*u) + return nil +} + +func (j URL) MarshalJSON() ([]byte, error) { + return json.Marshal(j.String()) +} + +func (j URL) String() string { + u := url.URL(j) + return u.String() +} + +func (j URL) JoinPath(elem ...string) URL { + u := url.URL(j) + return URL(*u.JoinPath(elem...)) +} + +func (j URL) Query() url.Values { + u := url.URL(j) + return u.Query() +} + +type Constraints semver.Constraints + +func (j *Constraints) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + c, err := semver.NewConstraint(raw) + if err != nil { + return err + } + + *j = Constraints(*c) + return nil +} + +func (j Constraints) MarshalJSON() ([]byte, error) { + return json.Marshal(j.String()) +} + +func (j Constraints) String() string { + return semver.Constraints(j).String() +} + +type SourceConfig struct { + URL URL `json:"url"` + Charts map[string][]string `json:"charts,omitempty"` + ChartsBySemver map[string]Constraints `json:"chartsBySemver,omitempty"` +} + +type SyncConfig struct { + Sources []SourceConfig `json:"sources"` +} + +type SyncOpts struct { + FailFast bool +} + +type SyncOpt interface { + Apply(*SyncOpts) +} + +func (s *SyncOpts) Apply(opts *SyncOpts) { + if s != nil { + if opts != nil { + opts.FailFast = s.FailFast + } else { + opts = s + } + } +} + +func (s *SyncConfig) Sync(ctx context.Context, dest string, opts ...SyncOpts) error { + o := &SyncOpts{} + + for _, opt := range opts { + opt.Apply(o) + } + + log := util.SloggerFrom(ctx) + + d, err := url.Parse(os.ExpandEnv(dest)) + if err != nil { + return err + } + + if d.Scheme == "" { + d.Scheme = "file" + } + + destination, ok := destMux[d.Scheme] + if !ok { + return fmt.Errorf("no destination registered for scheme: %s", d.Scheme) + } + + syncableDestination, ok := destination.(SyncableDestination) + if !ok { + return fmt.Errorf("not a syncable destination scheme: %s", d.Scheme) + } + + eg := new(errgroup.Group) + if o.FailFast { + eg, ctx = errgroup.WithContext(ctx) + } + + for _, src := range s.Sources { + eg.Go(func() error { + s, err := url.Parse(os.ExpandEnv(src.URL.String())) + if err != nil { + return err + } + + if s.Scheme == "" { + s.Scheme = "file" + } + + source, ok := srcMux[s.Scheme] + if !ok { + return fmt.Errorf("no source registered for scheme: %s", s.Scheme) + } + + if len(src.Charts) > 0 || len(src.ChartsBySemver) > 0 { + for name, versions := range src.Charts { + for _, version := range versions { + t := s.JoinPath(name) + q := t.Query() + q.Set("version", version) + t.RawQuery = q.Encode() + + chart, err := source.Open(ctx, t) + if err != nil { + return err + } + + eg.Go(func() error { + return syncableDestination.Sync(ctx, d, chart) + }) + } + } + + if len(src.ChartsBySemver) > 0 { + queryableSource, ok := source.(QueryableSource) + if !ok { + return fmt.Errorf("not a queryable source scheme: %s", s.Scheme) + } + + for name, constraints := range src.ChartsBySemver { + versions, err := queryableSource.Versions(ctx, s, name) + if err != nil { + return err + } + + for _, version := range versions { + v, err := semver.NewVersion(version) + if err != nil { + log.Debug("skipping invalid semver") + continue + } + + t := s.JoinPath(name) + q := t.Query() + q.Set("version", version) + t.RawQuery = q.Encode() + + chart, err := source.Open(ctx, t) + if err != nil { + return err + } + + if semver.Constraints(constraints).Check(v) { + eg.Go(func() error { + log.Info("syncing", "chart", chart.Name(), "version", chart.Metadata.Version, "destination", d.String()) + return syncableDestination.Sync(ctx, d, chart) + }) + } + } + } + } + + return nil + } + + chart, err := source.Open(ctx, s) + if err != nil { + return err + } + + return syncableDestination.Sync(ctx, d, chart) + }) + } + + return eg.Wait() +} + +func Sync(ctx context.Context, cfg, dest string) error { + b, err := os.ReadFile(cfg) + if err != nil { + return err + } + + s := &SyncConfig{} + if err := yaml.Unmarshal(b, s); err != nil { + return err + } + + return s.Sync(ctx, dest) +} diff --git a/sync_test.go b/sync_test.go new file mode 100644 index 0000000..f917560 --- /dev/null +++ b/sync_test.go @@ -0,0 +1,125 @@ +package barge_test + +import ( + "fmt" + "net/url" + "os" + "os/exec" + "path/filepath" + "testing" + + "dagger.io/dagger" + "github.com/frantjc/barge" + _ "github.com/frantjc/barge/internal/archive" + _ "github.com/frantjc/barge/internal/chartmuseum" + _ "github.com/frantjc/barge/internal/directory" + _ "github.com/frantjc/barge/internal/file" + _ "github.com/frantjc/barge/internal/http" + _ "github.com/frantjc/barge/internal/oci" + _ "github.com/frantjc/barge/internal/repo" + "github.com/frantjc/barge/testdata" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" +) + +func FuzzSync(f *testing.F) { + ctx := Context(f) + + tmp, err := os.CreateTemp(f.TempDir(), "test-0.1.0.tgz") + require.NoError(f, err) + _, err = tmp.Write(testdata.ChartArchive) + require.NoError(f, err) + require.NoError(f, tmp.Close()) + archiveURL, err := url.Parse(fmt.Sprintf("archive://%s", tmp.Name())) + require.NoError(f, err) + + tmp, err = os.CreateTemp(f.TempDir(), "archive-sync-config.yml") + require.NoError(f, err) + + b, err := yaml.Marshal(&barge.SyncConfig{ + Sources: []barge.SourceConfig{ + { + URL: barge.URL(*archiveURL), + }, + }, + }) + require.NoError(f, err) + _, err = tmp.Write(b) + require.NoError(f, err) + require.NoError(f, tmp.Close()) + archiveSyncCfg := tmp.Name() + + f.Add(archiveSyncCfg, os.TempDir()) + + if dag, err := dagger.Connect(ctx); assert.NoError(f, err) { + f.Cleanup(func() { + assert.NoError(f, dag.Close()) + }) + + chartmuseumURL := Chartmuseum(f, dag) + + if helm, err := exec.LookPath("helm"); assert.NoError(f, err) { + repo := "chartmuseum" + repoURL := url.URL{ + Scheme: "http", + Host: chartmuseumURL.Host, + } + add := Command(f, helm, "repo", "add", repo, repoURL.String()) + require.NoError(f, add.Run()) + } + + f.Add(archiveSyncCfg, chartmuseumURL.String()) + + registryURL := Registry(f, dag) + f.Add(archiveSyncCfg, registryURL.String()) + } + + f.Fuzz(func(t *testing.T, cfg, dest string) { + require.NoError(t, barge.Sync(ctx, cfg, dest)) + }) +} + +func FuzzSyncError(f *testing.F) { + ctx := Context(f) + + tmp, err := os.CreateTemp(f.TempDir(), "test-0.1.0.tgz") + require.NoError(f, err) + _, err = tmp.Write(testdata.ChartArchive) + require.NoError(f, err) + require.NoError(f, tmp.Close()) + archiveURL, err := url.Parse(fmt.Sprintf("archive://%s", tmp.Name())) + require.NoError(f, err) + + tmp, err = os.CreateTemp(f.TempDir(), "archive-sync-config.yml") + require.NoError(f, err) + + b, err := yaml.Marshal(&barge.SyncConfig{ + Sources: []barge.SourceConfig{ + { + URL: barge.URL(*archiveURL), + }, + }, + }) + require.NoError(f, err) + _, err = tmp.Write(b) + require.NoError(f, err) + require.NoError(f, tmp.Close()) + cfg := tmp.Name() + + f.Add(cfg, "invalid://") + f.Add(cfg, "oci://does-not-exist") + + if dag, err := dagger.Connect(ctx); assert.NoError(f, err) { + f.Cleanup(func() { + assert.NoError(f, dag.Close()) + }) + + registryURL := Registry(f, dag) + f.Add(filepath.Join(f.TempDir(), "does-not-exist.yaml"), registryURL.String()) + } + + f.Fuzz(func(t *testing.T, cfg, dest string) { + assert.Error(t, barge.Sync(ctx, cfg, dest)) + }) +}