diff --git a/pkg/porter/generateManifest.go b/pkg/porter/generateManifest.go index abcc52cbc..4c0a5e941 100644 --- a/pkg/porter/generateManifest.go +++ b/pkg/porter/generateManifest.go @@ -7,6 +7,7 @@ import ( "strings" cnabtooci "get.porter.sh/porter/pkg/cnab/cnab-to-oci" + "get.porter.sh/porter/pkg/experimental" "get.porter.sh/porter/pkg" "get.porter.sh/porter/pkg/build" @@ -14,6 +15,7 @@ import ( "get.porter.sh/porter/pkg/manifest" "get.porter.sh/porter/pkg/tracing" "get.porter.sh/porter/pkg/yaml" + "github.com/docker/distribution/reference" "github.com/mikefarah/yq/v3/pkg/yqlib" "github.com/opencontainers/go-digest" "go.opentelemetry.io/otel/attribute" @@ -111,15 +113,83 @@ func (p *Porter) generateInternalManifest(ctx context.Context, opts BuildOptions } } return e.SetValue(path+"digest", digest.String()) - }) if err != nil { return err } + if p.IsFeatureEnabled(experimental.FlagDependenciesV2) { + if err = p.resolveDependencyDigest(ctx, e, regOpts); err != nil { + return err + } + } + return e.WriteFile(build.LOCAL_MANIFEST) } +func (p *Porter) resolveDependencyDigest(ctx context.Context, e *yaml.Editor, opts cnabtooci.RegistryOptions) error { + // find all referenced dependencies that does not have digest specified + // get the digest for all of them and update the manifest with the digest + return e.WalkNodes(ctx, "dependencies.requires.*", func(ctx context.Context, nc *yqlib.NodeContext) error { + ctx, span := tracing.StartSpanWithName(ctx, "updateDependencyTagToDigest") + defer span.EndSpan() + + dep := &manifest.Dependency{} + if err := nc.Node.Decode(dep); err != nil { + return span.Errorf("failed to deserialize dependency in manifest: %w", err) + } + + span.SetAttributes(attribute.String("dependency", dep.Name)) + + bundleOpts := BundleReferenceOptions{ + BundlePullOptions: BundlePullOptions{ + Reference: dep.Bundle.Reference, + InsecureRegistry: opts.InsecureRegistry, + }, + } + + ref, err := cnab.ParseOCIReference(dep.Bundle.Reference) + if err != nil { + return span.Errorf("failed to parse OCI reference for dependency %s: %w", dep.Name, err) + } + + if ref.Tag() == "" || ref.Tag() == "latest" { + return nil + } + + bundleRef, err := p.resolveBundleReference(ctx, &bundleOpts) + if err != nil { + return span.Errorf("failed to resolve dependency %s: %w", dep.Name, err) + } + + digest := bundleRef.Digest + span.SetAttributes(attribute.String("digest", digest.Encoded())) + + var path string + for _, p := range nc.PathStack { + switch t := p.(type) { + case string: + path += fmt.Sprintf("%s.", t) + case int: + path = strings.TrimSuffix(path, ".") + path += fmt.Sprintf("[%s].", strconv.Itoa(t)) + default: + continue + } + } + + newRef := cnab.OCIReference{ + Named: reference.TrimNamed(bundleRef.Reference.Named), + } + refWithDigest, err := newRef.WithDigest(digest) + if err != nil { + return span.Errorf("failed to set digest: %w", err) + } + + return e.SetValue(path+"bundle.reference", refWithDigest.String()) + }) +} + // getImageDigest retrieves the repository digest associated with the specified image reference. func (p *Porter) getImageDigest(ctx context.Context, img cnab.OCIReference, regOpts cnabtooci.RegistryOptions) (digest.Digest, error) { ctx, span := tracing.StartSpan(ctx, attribute.String("image", img.String())) diff --git a/pkg/porter/generateManifest_test.go b/pkg/porter/generateManifest_test.go index a389181fc..e756fcb52 100644 --- a/pkg/porter/generateManifest_test.go +++ b/pkg/porter/generateManifest_test.go @@ -8,9 +8,11 @@ import ( "testing" "get.porter.sh/porter/pkg/build" + "get.porter.sh/porter/pkg/cache" "get.porter.sh/porter/pkg/cnab" cnabtooci "get.porter.sh/porter/pkg/cnab/cnab-to-oci" "get.porter.sh/porter/pkg/config" + "get.porter.sh/porter/pkg/experimental" "get.porter.sh/porter/pkg/test" "github.com/docker/docker/api/types" "github.com/stretchr/testify/require" @@ -184,3 +186,80 @@ func Test_getImageLatestDigest(t *testing.T) { }) } } + +func Test_depv2_bundleDigest(t *testing.T) { + defaultMockFindBundle := func(ref cnab.OCIReference) (cache.CachedBundle, bool, error) { + cachedBundle := cache.CachedBundle{ + BundleReference: cnab.BundleReference{ + Reference: ref, + Digest: "sha256:3abc67269f59e3ed824e811a1ff1ee64f0d44c0218efefada57a4bebc2d7ef6f", + }, + } + + return cachedBundle, true, nil + } + + testcases := []struct { + name string + originalManifest string + wantManifest string + wantErr string + mockFindBundle func(ref cnab.OCIReference) (cache.CachedBundle, bool, error) + mockPullBundle func(ctx context.Context, ref cnab.OCIReference, opts cnabtooci.RegistryOptions) (cnab.BundleReference, error) + }{ + { + name: "use digest in bundle reference", + wantManifest: "expected-result-depv2.yaml", + originalManifest: "original-depv2.yaml", + }, + { + name: "not found reference", + wantManifest: "expected-result-depv2.yaml", + originalManifest: "original-depv2.yaml", + mockFindBundle: func(ref cnab.OCIReference) (cache.CachedBundle, bool, error) { + return cache.CachedBundle{}, false, nil + }, + mockPullBundle: func(ctx context.Context, ref cnab.OCIReference, opts cnabtooci.RegistryOptions) (cnab.BundleReference, error) { + return cnab.BundleReference{}, errors.New("failed to pull bundle") + }, + wantErr: "failed to pull bundle", + }, + { + name: "no default bundle reference", + wantManifest: "expected-result-depv2-no-default-ref.yaml", + originalManifest: "original-depv2-no-default-ref.yaml", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + p := NewTestPorter(t) + p.SetExperimentalFlags(experimental.FlagDependenciesV2) + defer p.Close() + if tc.mockFindBundle != nil { + p.TestCache.FindBundleMock = tc.mockFindBundle + } else { + p.TestCache.FindBundleMock = defaultMockFindBundle + } + if tc.mockPullBundle != nil { + p.TestRegistry.MockPullBundle = tc.mockPullBundle + } + p.TestConfig.TestContext.AddTestFile(filepath.Join("testdata/generateManifest", tc.originalManifest), config.Name) + opts := BuildOptions{} + opts.Validate(p.Porter) + + err := p.generateInternalManifest(context.Background(), opts) + if tc.wantErr != "" { + require.ErrorContains(t, err, tc.wantErr) + return + } + require.NoError(t, err) + + goldenFile := filepath.Join("testdata/generateManifest", tc.wantManifest) + p.TestConfig.TestContext.AddTestFile(goldenFile, tc.wantManifest) + got, err := p.FileSystem.ReadFile(build.LOCAL_MANIFEST) + require.NoError(t, err) + test.CompareGoldenFile(t, goldenFile, string(got)) + }) + } +} diff --git a/pkg/porter/testdata/generateManifest/expected-result-depv2-no-default-ref.yaml b/pkg/porter/testdata/generateManifest/expected-result-depv2-no-default-ref.yaml new file mode 100644 index 000000000..95c83f203 --- /dev/null +++ b/pkg/porter/testdata/generateManifest/expected-result-depv2-no-default-ref.yaml @@ -0,0 +1,33 @@ +schemaVersion: 1.0.0-alpha.1 +name: porter-hello +version: 0.1.0 +description: "An example Porter configuration" +registry: "localhost:5000" +mixins: + - exec +dependencies: + requires: + - name: mysql + bundle: + reference: getporter/mysql + - name: mysqlLatest + bundle: + reference: getporter/mysql:latest +install: + - exec: + description: "Install Hello World" + command: ./helpers.sh + arguments: + - install +status: + - exec: + description: "World Status" + command: ./helpers.sh + arguments: + - status +uninstall: + - exec: + description: "Uninstall Hello World" + command: ./helpers.sh + arguments: + - uninstall diff --git a/pkg/porter/testdata/generateManifest/expected-result-depv2.yaml b/pkg/porter/testdata/generateManifest/expected-result-depv2.yaml new file mode 100644 index 000000000..8389e02ef --- /dev/null +++ b/pkg/porter/testdata/generateManifest/expected-result-depv2.yaml @@ -0,0 +1,30 @@ +schemaVersion: 1.0.0-alpha.1 +name: porter-hello +version: 0.1.0 +description: "An example Porter configuration" +registry: "localhost:5000" +mixins: + - exec +dependencies: + requires: + - name: mysql + bundle: + reference: getporter/mysql@sha256:3abc67269f59e3ed824e811a1ff1ee64f0d44c0218efefada57a4bebc2d7ef6f +install: + - exec: + description: "Install Hello World" + command: ./helpers.sh + arguments: + - install +status: + - exec: + description: "World Status" + command: ./helpers.sh + arguments: + - status +uninstall: + - exec: + description: "Uninstall Hello World" + command: ./helpers.sh + arguments: + - uninstall diff --git a/pkg/porter/testdata/generateManifest/original-depv2-no-default-ref.yaml b/pkg/porter/testdata/generateManifest/original-depv2-no-default-ref.yaml new file mode 100644 index 000000000..95c83f203 --- /dev/null +++ b/pkg/porter/testdata/generateManifest/original-depv2-no-default-ref.yaml @@ -0,0 +1,33 @@ +schemaVersion: 1.0.0-alpha.1 +name: porter-hello +version: 0.1.0 +description: "An example Porter configuration" +registry: "localhost:5000" +mixins: + - exec +dependencies: + requires: + - name: mysql + bundle: + reference: getporter/mysql + - name: mysqlLatest + bundle: + reference: getporter/mysql:latest +install: + - exec: + description: "Install Hello World" + command: ./helpers.sh + arguments: + - install +status: + - exec: + description: "World Status" + command: ./helpers.sh + arguments: + - status +uninstall: + - exec: + description: "Uninstall Hello World" + command: ./helpers.sh + arguments: + - uninstall diff --git a/pkg/porter/testdata/generateManifest/original-depv2.yaml b/pkg/porter/testdata/generateManifest/original-depv2.yaml new file mode 100644 index 000000000..a1eb283f2 --- /dev/null +++ b/pkg/porter/testdata/generateManifest/original-depv2.yaml @@ -0,0 +1,30 @@ +schemaVersion: 1.0.0-alpha.1 +name: porter-hello +version: 0.1.0 +description: "An example Porter configuration" +registry: "localhost:5000" +mixins: + - exec +dependencies: + requires: + - name: mysql + bundle: + reference: getporter/mysql:v0.1.4 +install: + - exec: + description: "Install Hello World" + command: ./helpers.sh + arguments: + - install +status: + - exec: + description: "World Status" + command: ./helpers.sh + arguments: + - status +uninstall: + - exec: + description: "Uninstall Hello World" + command: ./helpers.sh + arguments: + - uninstall