From 7e8bf5ab107db3d23213640276886955eba7bc3b Mon Sep 17 00:00:00 2001 From: Paulo Sousa Date: Fri, 13 Sep 2024 10:52:26 -0300 Subject: [PATCH] feat: add option to create remote repository before first push fix lint --- go.mod | 7 +- go.sum | 13 +++- main.go | 17 ++++- pkg/build/buildkit/build.go | 53 ++++++++++++- pkg/build/buildkit/build_test.go | 24 ++++++ pkg/build/helpers.go | 14 ++++ pkg/repository/fake/repository.go | 46 ++++++++++++ pkg/repository/oci/oci.go | 97 ++++++++++++++++++++++++ pkg/repository/oci/oci_test.go | 119 ++++++++++++++++++++++++++++++ pkg/repository/repository.go | 54 ++++++++++++++ pkg/repository/repository_test.go | 44 +++++++++++ 11 files changed, 481 insertions(+), 7 deletions(-) create mode 100644 pkg/repository/fake/repository.go create mode 100644 pkg/repository/oci/oci.go create mode 100644 pkg/repository/oci/oci_test.go create mode 100644 pkg/repository/repository.go create mode 100644 pkg/repository/repository_test.go diff --git a/go.mod b/go.mod index afaca76..bc99ab0 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,8 @@ require ( github.com/docker/docker v23.0.0-rc.1+incompatible github.com/google/go-containerregistry v0.12.0 github.com/moby/buildkit v0.11.3 - github.com/stretchr/testify v1.8.0 + github.com/oracle/oci-go-sdk/v65 v65.73.0 + github.com/stretchr/testify v1.8.4 golang.org/x/sync v0.1.0 google.golang.org/grpc v1.50.1 google.golang.org/protobuf v1.28.1 @@ -61,7 +62,9 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect + github.com/sony/gobreaker v0.5.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/tonistiigi/fsutil v0.0.0-20230105215944-fb433841cbfa // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f // indirect @@ -75,7 +78,7 @@ require ( golang.org/x/mod v0.6.0 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/oauth2 v0.1.0 // indirect - golang.org/x/sys v0.5.0 // indirect + golang.org/x/sys v0.8.0 // indirect golang.org/x/term v0.5.0 // indirect golang.org/x/text v0.7.0 // indirect golang.org/x/time v0.1.0 // indirect diff --git a/go.sum b/go.sum index 8b239b4..8c77421 100644 --- a/go.sum +++ b/go.sum @@ -322,6 +322,8 @@ github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuh github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/oracle/oci-go-sdk/v65 v65.73.0 h1:C7uel6CoKk4A1KPkdhFBAyvVyFRTHAmX8m0o64RmfPg= +github.com/oracle/oci-go-sdk/v65 v65.73.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= github.com/package-url/packageurl-go v0.1.1-0.20220428063043-89078438f170 h1:DiLBVp4DAcZlBVBEtJpNWZpZVq0AEeCY7Hqk8URVs4o= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= @@ -344,6 +346,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spdx/tools-golang v0.3.1-0.20230104082527-d6f58551be3f h1:9B623Cfs+mclYK6dsae7gLSwuIBHvlgmEup87qpqsAQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -353,6 +357,8 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -360,8 +366,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tonistiigi/fsutil v0.0.0-20230105215944-fb433841cbfa h1:XOFp/3aBXlqmOFAg3r6e0qQjPnK5I970LilqX+Is1W8= github.com/tonistiigi/fsutil v0.0.0-20230105215944-fb433841cbfa/go.mod h1:AvLEd1LEIl64G2Jpgwo7aVV5lGH0ePcKl0ygGIHNYl8= @@ -569,8 +576,8 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/main.go b/main.go index 6b99870..3ad9e24 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ import ( "github.com/tsuru/deploy-agent/pkg/build/buildkit" buildpb "github.com/tsuru/deploy-agent/pkg/build/grpc_build_v1" "github.com/tsuru/deploy-agent/pkg/health" + "github.com/tsuru/deploy-agent/pkg/repository" ) const ( @@ -42,12 +43,13 @@ var cfg struct { BuildKitAutoDiscoveryKubernetesLeasePrefix string BuildKitAutoDiscoveryStatefulset string KubernetesConfig string - BuildKitAutoDiscoveryScaleGracefulPeriod time.Duration + RemoteRepositoryPath string BuildKitAutoDiscoveryTimeout time.Duration BuildKitAutoDiscoveryKubernetesPort int Port int ServerMaxRecvMsgSize int ServerMaxSendMsgSize int + BuildKitAutoDiscoveryScaleGracefulPeriod time.Duration BuildKitAutoDiscovery bool BuildKitAutoDiscoveryKubernetesSetTsuruAppLabels bool BuildKitAutoDiscoveryKubernetesUseSameNamespaceAsTsuruApp bool @@ -65,6 +67,8 @@ func main() { flag.StringVar(&cfg.BuildkitAddress, "buildkit-addr", getEnvOrDefault("BUILDKIT_HOST", ""), "Buildkit server address") flag.StringVar(&cfg.BuildkitTmpDir, "buildkit-tmp-dir", os.TempDir(), "Directory path to store temp files during container image builds") + flag.StringVar(&cfg.RemoteRepositoryPath, "remote-repository-path", getEnvOrDefault("REMOTE_REPOSITORY_PATH", ""), "Remote image repository providers config path") + flag.BoolVar(&cfg.BuildKitAutoDiscovery, "buildkit-autodiscovery", false, "Whether should dynamically discover the BuildKit service based on Tsuru app (if any)") flag.DurationVar(&cfg.BuildKitAutoDiscoveryTimeout, "buildkit-autodiscovery-timeout", (5 * time.Minute), "Max duration to discover an available BuildKit") flag.StringVar(&cfg.BuildKitAutoDiscoveryKubernetesPodSelector, "buildkit-autodiscovery-kubernetes-pod-selector", "", "Label selector of BuildKit's pods on Kubernetes") @@ -148,6 +152,17 @@ func newBuildKit() (*buildkit.BuildKit, error) { c = bc } + if cfg.RemoteRepositoryPath != "" { + repositoryData, err := os.ReadFile(cfg.RemoteRepositoryPath) + if err != nil { + return nil, err + } + opts.RemoteRepository, err = repository.NewRemoteRepository(repositoryData) + if err != nil { + return nil, fmt.Errorf("failed to handle remote repository cfg: %w", err) + } + } + b := buildkit.NewBuildKit(c, opts) if cfg.BuildKitAutoDiscovery { diff --git a/pkg/build/buildkit/build.go b/pkg/build/buildkit/build.go index 2bf8d4d..4279bdb 100644 --- a/pkg/build/buildkit/build.go +++ b/pkg/build/buildkit/build.go @@ -40,12 +40,14 @@ import ( "github.com/tsuru/deploy-agent/pkg/build" "github.com/tsuru/deploy-agent/pkg/build/buildkit/scaler" pb "github.com/tsuru/deploy-agent/pkg/build/grpc_build_v1" + repo "github.com/tsuru/deploy-agent/pkg/repository" "github.com/tsuru/deploy-agent/pkg/util" ) var _ build.Builder = (*BuildKit)(nil) type BuildKitOptions struct { + RemoteRepository map[string]repo.Repository TempDir string DiscoverBuildKitClientForApp bool } @@ -163,6 +165,13 @@ func (b *BuildKit) buildFromAppSourceFiles(ctx context.Context, c *client.Client } defer cleanFunc() + if b.opts.RemoteRepository != nil { + err = b.createRemoteRepository(ctx, r) + if err != nil { + return nil, err + } + } + if err = callBuildKitBuild(ctx, c, tmpDir, r, w); err != nil { return nil, err } @@ -219,6 +228,13 @@ func (b *BuildKit) buildFromContainerImage(ctx context.Context, c *client.Client } defer cleanFunc() + if b.opts.RemoteRepository != nil { + err = b.createRemoteRepository(ctx, r) + if err != nil { + return nil, err + } + } + if err = callBuildKitBuild(ctx, c, tmpDir, r, w); err != nil { return nil, err } @@ -252,6 +268,29 @@ func (b *BuildKit) extractTsuruConfigsFromContainerImage(ctx context.Context, c return callBuildKitToExtractTsuruConfigs(ctx, c, tmpDir, workingDir) } +func (b *BuildKit) createRemoteRepository(ctx context.Context, r *pb.BuildRequest) error { + for _, v := range r.DestinationImages { + if provider, ok := b.opts.RemoteRepository[build.GetRegistry(v)]; ok { + err := provider.Auth(ctx) + if err != nil { + return err + } + exists, err := provider.Exists(ctx, v) + if err != nil { + return err + } + if exists { + continue + } + err = provider.Create(ctx, v) + if err != nil { + return err + } + } + } + return nil +} + func extractContainerImageConfigFromImageManifest(ctx context.Context, imageStr string, insecureRegistry bool) (*pb.ContainerImageConfig, error) { if err := ctx.Err(); err != nil { return nil, err @@ -388,6 +427,13 @@ func (b *BuildKit) buildFromContainerFile(ctx context.Context, c *client.Client, } defer cleanFunc() + if b.opts.RemoteRepository != nil { + err = b.createRemoteRepository(ctx, r) + if err != nil { + return nil, err + } + } + if err = callBuildKitBuild(ctx, c, tmpDir, r, w); err != nil { return nil, err } @@ -418,7 +464,12 @@ func (b *BuildKit) buildPlatform(ctx context.Context, c *client.Client, r *pb.Bu return err } defer cleanFunc() - + if b.opts.RemoteRepository != nil { + err = b.createRemoteRepository(ctx, r) + if err != nil { + return err + } + } return callBuildKitBuild(ctx, c, tmpDir, r, w) } diff --git a/pkg/build/buildkit/build_test.go b/pkg/build/buildkit/build_test.go index 780249e..8a1af52 100644 --- a/pkg/build/buildkit/build_test.go +++ b/pkg/build/buildkit/build_test.go @@ -26,6 +26,8 @@ import ( . "github.com/tsuru/deploy-agent/pkg/build/buildkit" pb "github.com/tsuru/deploy-agent/pkg/build/grpc_build_v1" + "github.com/tsuru/deploy-agent/pkg/repository" + "github.com/tsuru/deploy-agent/pkg/repository/fake" "github.com/tsuru/deploy-agent/pkg/util" ) @@ -376,6 +378,28 @@ func TestBuildKit_Build_FromContainerImages(t *testing.T) { }, appFiles) }) + t.Run("creating remote repository", func(t *testing.T) { + req := &pb.BuildRequest{ + Kind: pb.BuildKind_BUILD_KIND_APP_DEPLOY_WITH_CONTAINER_IMAGE, + App: &pb.TsuruApp{ + Name: "my-app", + }, + SourceImage: "nginx:1.22-alpine", + DestinationImages: []string{baseRegistry(t, "app-my-app", "v1")}, + PushOptions: &pb.PushOptions{InsecureRegistry: registryHTTP}, + } + opts := &BuildKitOptions{TempDir: t.TempDir(), RemoteRepository: map[string]repository.Repository{registryAddress: &fake.FakeRepository{AuthSuccess: true}}} + assert.Equal(t, opts.RemoteRepository[registryAddress].(*fake.FakeRepository).RepoExists, map[string]bool(nil)) + _, err := NewBuildKit(bc, *opts). + Build(context.TODO(), req, os.Stdout) + require.NoError(t, err) + assert.Equal(t, opts.RemoteRepository[registryAddress].(*fake.FakeRepository).RepoExists, map[string]bool{baseRegistry(t, "app-my-app", "v1"): true}) + _, err = NewBuildKit(bc, *opts). + Build(context.TODO(), req, os.Stdout) + require.NoError(t, err) + assert.Equal(t, opts.RemoteRepository[registryAddress].(*fake.FakeRepository).RepoExists, map[string]bool{baseRegistry(t, "app-my-app", "v1"): true}) + }) + t.Run("container image without Tsuru app files (tsuru.yaml, Procfile) + job image push", func(t *testing.T) { req := &pb.BuildRequest{ Kind: pb.BuildKind_BUILD_KIND_JOB_CREATE_WITH_CONTAINER_IMAGE, diff --git a/pkg/build/helpers.go b/pkg/build/helpers.go index b8fe661..2f926d4 100644 --- a/pkg/build/helpers.go +++ b/pkg/build/helpers.go @@ -293,3 +293,17 @@ func SortExposedPorts(ports map[string]struct{}) []string { return ps } + +func GetRegistry(image string) string { + defaultRegistry := "docker.io" + if !strings.Contains(image, "/") { + return defaultRegistry + } + + registry := strings.Split(image, "/")[0] + if strings.Contains(registry, ".") || strings.Contains(registry, ":") { + return registry + } + + return defaultRegistry +} diff --git a/pkg/repository/fake/repository.go b/pkg/repository/fake/repository.go new file mode 100644 index 0000000..4abecb6 --- /dev/null +++ b/pkg/repository/fake/repository.go @@ -0,0 +1,46 @@ +// Copyright 2024 tsuru authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fake + +import ( + "context" + "errors" +) + +type FakeRepository struct { + CreatedRepos map[string]bool + RepoExists map[string]bool + AuthSuccess bool +} + +func (f *FakeRepository) Auth(ctx context.Context) error { + if f.AuthSuccess { + return nil + } + return errors.New("auth repository failed") +} + +func (f *FakeRepository) Create(ctx context.Context, name string) error { + if _, exists := f.RepoExists[name]; exists { + return errors.New("repository already exists") + } + if f.CreatedRepos == nil { + f.CreatedRepos = make(map[string]bool) + } + if f.RepoExists == nil { + f.RepoExists = make(map[string]bool) + } + f.CreatedRepos[name] = true + f.RepoExists[name] = true + return nil +} + +func (f *FakeRepository) Exists(ctx context.Context, name string) (bool, error) { + if f.RepoExists == nil { + f.RepoExists = make(map[string]bool) + } + exists := f.RepoExists[name] + return exists, nil +} diff --git a/pkg/repository/oci/oci.go b/pkg/repository/oci/oci.go new file mode 100644 index 0000000..e34ec21 --- /dev/null +++ b/pkg/repository/oci/oci.go @@ -0,0 +1,97 @@ +// Copyright 2024 tsuru authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package oci + +import ( + "context" + "fmt" + "strings" + + "github.com/oracle/oci-go-sdk/v65/artifacts" + "github.com/oracle/oci-go-sdk/v65/common" +) + +type OCIRequiredMethods interface { + CreateContainerRepository(ctx context.Context, request artifacts.CreateContainerRepositoryRequest) (response artifacts.CreateContainerRepositoryResponse, err error) + ListContainerRepositories(ctx context.Context, request artifacts.ListContainerRepositoriesRequest) (response artifacts.ListContainerRepositoriesResponse, err error) +} + +type OCI struct { + client OCIRequiredMethods + CompartmentID string + Profile string + ConfigPath string +} + +func NewOCI(data map[string]string) *OCI { + return &OCI{ + CompartmentID: data["compartmentID"], + Profile: data["profile"], + ConfigPath: data["configPath"], + client: &artifacts.ArtifactsClient{}, + } +} + +func (r *OCI) Auth(ctx context.Context) error { + if r.client != nil { + return nil + } + configProvider := common.CustomProfileConfigProvider(r.ConfigPath, r.Profile) + client, err := artifacts.NewArtifactsClientWithConfigurationProvider(configProvider) + r.client = &client + if err != nil { + return err + } + return err +} + +func (r *OCI) Create(ctx context.Context, name string) error { + name, err := parserRegistryRepository(name) + if err != nil { + return err + } + request := artifacts.CreateContainerRepositoryRequest{ + CreateContainerRepositoryDetails: artifacts.CreateContainerRepositoryDetails{ + CompartmentId: &r.CompartmentID, + DisplayName: common.String(name), + }, + } + _, err = r.client.CreateContainerRepository(ctx, request) + if err != nil { + return err + } + return nil +} + +func (r *OCI) Exists(ctx context.Context, name string) (bool, error) { + name, err := parserRegistryRepository(name) + if err != nil { + return false, err + } + request := artifacts.ListContainerRepositoriesRequest{ + CompartmentId: &r.CompartmentID, + DisplayName: common.String(name), + } + response, err := r.client.ListContainerRepositories(ctx, request) + if err != nil { + return false, err + } + if len(response.ContainerRepositoryCollection.Items) == 0 { + return false, nil + } + return true, nil +} + +func parserRegistryRepository(image string) (string, error) { + parts := strings.Split(image, "/") + if len(parts) < 3 { + return "", fmt.Errorf("invalid image format %s", image) + } + repoWithTag := strings.Join(parts[2:], "/") + repoParts := strings.Split(repoWithTag, ":") + repoWithoutTag := repoParts[0] + + return repoWithoutTag, nil +} diff --git a/pkg/repository/oci/oci_test.go b/pkg/repository/oci/oci_test.go new file mode 100644 index 0000000..063a865 --- /dev/null +++ b/pkg/repository/oci/oci_test.go @@ -0,0 +1,119 @@ +// Copyright 2024 tsuru authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package oci + +import ( + "context" + "errors" + "testing" + + "github.com/oracle/oci-go-sdk/v65/artifacts" + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/stretchr/testify/assert" +) + +type FakeArtifactsClient struct { + repo map[string]bool + artifacts.ArtifactsClient +} + +func (m *FakeArtifactsClient) CreateContainerRepository(ctx context.Context, request artifacts.CreateContainerRepositoryRequest) (response artifacts.CreateContainerRepositoryResponse, err error) { + repo := *request.CreateContainerRepositoryDetails.DisplayName + if _, ok := m.repo[repo]; ok { + return artifacts.CreateContainerRepositoryResponse{}, errors.New("repository already exists") + } + if m.repo == nil { + m.repo = make(map[string]bool) + } + m.repo[repo] = true + return artifacts.CreateContainerRepositoryResponse{}, nil +} + +func (m *FakeArtifactsClient) ListContainerRepositories(ctx context.Context, request artifacts.ListContainerRepositoriesRequest) (response artifacts.ListContainerRepositoriesResponse, err error) { + repos := make([]artifacts.ContainerRepositorySummary, 0, 10) + for repo := range m.repo { + repos = append(repos, artifacts.ContainerRepositorySummary{DisplayName: common.String(repo)}) + } + return artifacts.ListContainerRepositoriesResponse{ + ContainerRepositoryCollection: artifacts.ContainerRepositoryCollection{ + Items: repos, + }, + }, nil +} + +func TestOCI_Create(t *testing.T) { + fakeClient := new(FakeArtifactsClient) + oci := &OCI{ + client: fakeClient, + } + ctx := context.TODO() + name := "registry/namespace/test-repo" + err := oci.Create(ctx, name) + assert.NoError(t, err) + err = oci.Create(ctx, name) + assert.Error(t, err, "repository already exists") +} + +func TestOCI_Exists(t *testing.T) { + fakeClient := new(FakeArtifactsClient) + oci := &OCI{ + client: fakeClient, + } + ctx := context.TODO() + name := "registry/namespace/test-repo" + exists, err := oci.Exists(ctx, name) + assert.NoError(t, err) + assert.False(t, exists) + err = oci.Create(ctx, name) + assert.NoError(t, err) + exists, err = oci.Exists(ctx, name) + assert.NoError(t, err) + assert.True(t, exists) +} +func TestParserRegistryRepository(t *testing.T) { + tests := []struct { + name string + image string + expected string + expectedErr bool + }{ + { + name: "No slash", + image: "image", + expected: "", + expectedErr: true, + }, + { + name: "One slash", + image: "registry/image", + expected: "", + expectedErr: true, + }, + { + name: "Multiple slashes", + image: "registry/namespace/image", + expected: "image", + expectedErr: false, + }, + { + name: "With tag", + image: "registry/namespace/image:tag", + expected: "image", + expectedErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := parserRegistryRepository(tt.image) + if tt.expectedErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go new file mode 100644 index 0000000..be8ad4d --- /dev/null +++ b/pkg/repository/repository.go @@ -0,0 +1,54 @@ +// Copyright 2024 tsuru authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package repository + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/tsuru/deploy-agent/pkg/repository/fake" + "github.com/tsuru/deploy-agent/pkg/repository/oci" +) + +type Repository interface { + Auth(ctx context.Context) error + Create(ctx context.Context, name string) error + Exists(ctx context.Context, name string) (bool, error) +} + +type RemoteRepositoryProvider map[string]map[string]string + +func repositoryProvider(providerType string, data map[string]string) (Repository, error) { + switch providerType { + case "oci": + return oci.NewOCI(data), nil + case "fake": + return &fake.FakeRepository{}, nil + default: + return nil, fmt.Errorf("unknow repositoy provider: %s", providerType) + } +} + +func NewRemoteRepository(data []byte) (map[string]Repository, error) { + var remoteRepositoryProvider RemoteRepositoryProvider + err := json.Unmarshal(data, &remoteRepositoryProvider) + if err != nil { + return nil, err + } + var repositoryMap = make(map[string]Repository) + for k, v := range remoteRepositoryProvider { + if p, ok := v["provider"]; ok { + provider, err := repositoryProvider(p, v) + if err != nil { + return nil, err + } + repositoryMap[k] = provider + continue + } + return nil, fmt.Errorf("provider key not found in repository %s", k) + } + return repositoryMap, nil +} diff --git a/pkg/repository/repository_test.go b/pkg/repository/repository_test.go new file mode 100644 index 0000000..e84b435 --- /dev/null +++ b/pkg/repository/repository_test.go @@ -0,0 +1,44 @@ +// Copyright 2024 tsuru authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package repository + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tsuru/deploy-agent/pkg/repository/fake" + "github.com/tsuru/deploy-agent/pkg/repository/oci" +) + +func TestNewRemoteRepository(t *testing.T) { + data := []byte(`{ + "test.com": { + "provider": "oci", + "compartmentID": "123", + "profile": "dev" + }, + "faker.com": { + "provider": "fake" + } + }`) + repositoryMap, err := NewRemoteRepository(data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assert.Len(t, repositoryMap, 2) + assert.Equal(t, oci.NewOCI(map[string]string{"compartmentID": "123", "profile": "dev"}), repositoryMap["test.com"]) + assert.Equal(t, &fake.FakeRepository{}, repositoryMap["faker.com"].(*fake.FakeRepository)) +} + +func TestNewRepositoryInvalidProvider(t *testing.T) { + data := []byte(`{ + "test.com": { + "provider": "invalid" + } + }`) + _, err := NewRemoteRepository(data) + assert.Error(t, err) + assert.Equal(t, "unknow repositoy provider: invalid", err.Error()) +}