mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-08-07 06:22:17 -04:00
hack: add oci-pin tool
This tool can generate Go source files and lockfiles for container images.
This commit is contained in:
parent
4b9bce9bb7
commit
9d25372e10
14 changed files with 1692 additions and 0 deletions
27
hack/oci-pin/BUILD.bazel
Normal file
27
hack/oci-pin/BUILD.bazel
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "oci-pin_lib",
|
||||||
|
srcs = [
|
||||||
|
"codegen.go",
|
||||||
|
"merge.go",
|
||||||
|
"oci-pin.go",
|
||||||
|
"sum.go",
|
||||||
|
],
|
||||||
|
importpath = "github.com/edgelesssys/constellation/v2/hack/oci-pin",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//hack/oci-pin/internal/extract",
|
||||||
|
"//hack/oci-pin/internal/inject",
|
||||||
|
"//hack/oci-pin/internal/sums",
|
||||||
|
"//internal/logger",
|
||||||
|
"@com_github_spf13_cobra//:cobra",
|
||||||
|
"@org_uber_go_zap//zapcore",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "oci-pin",
|
||||||
|
embed = [":oci-pin_lib"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
176
hack/oci-pin/codegen.go
Normal file
176
hack/oci-pin/codegen.go
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/hack/oci-pin/internal/extract"
|
||||||
|
"github.com/edgelesssys/constellation/v2/hack/oci-pin/internal/inject"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newCodegenCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "codegen",
|
||||||
|
Short: "Generate Go code that pins an OCI image.",
|
||||||
|
RunE: runCodegen,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().String("oci-path", "", "Path to the OCI image to pin.")
|
||||||
|
cmd.Flags().String("output", "-", "Output file. If not set, the output is written to stdout.")
|
||||||
|
cmd.Flags().String("package", "", "Name of the Go package.")
|
||||||
|
cmd.Flags().String("identifier", "", "Base name of the Go const identifiers.")
|
||||||
|
cmd.Flags().String("image-registry", "", "Registry where the image is stored.")
|
||||||
|
cmd.Flags().String("image-prefix", "", "Prefix of the image name. Optional.")
|
||||||
|
cmd.Flags().String("image-name", "", "Short name of the OCI image to pin.")
|
||||||
|
cmd.Flags().String("image-tag", "", "Tag of the OCI image to pin. Optional.")
|
||||||
|
cmd.Flags().String("image-tag-file", "", "Tag file of the OCI image to pin. Optional.")
|
||||||
|
cmd.MarkFlagsMutuallyExclusive("image-tag", "image-tag-file")
|
||||||
|
must(cmd.MarkFlagRequired("oci-path"))
|
||||||
|
must(cmd.MarkFlagRequired("package"))
|
||||||
|
must(cmd.MarkFlagRequired("identifier"))
|
||||||
|
must(cmd.MarkFlagRequired("image-registry"))
|
||||||
|
must(cmd.MarkFlagRequired("image-name"))
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCodegen(cmd *cobra.Command, _ []string) error {
|
||||||
|
flags, err := parseCodegenFlags(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log := logger.New(logger.PlainLog, flags.logLevel)
|
||||||
|
log.Debugf("Parsed flags: %+v", flags)
|
||||||
|
|
||||||
|
log.Debugf("Generating Go code for OCI image %s.", flags.imageName)
|
||||||
|
|
||||||
|
ociIndexPath := filepath.Join(flags.ociPath, "index.json")
|
||||||
|
index, err := os.Open(ociIndexPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("opening OCI index at %q: %w", ociIndexPath, err)
|
||||||
|
}
|
||||||
|
defer index.Close()
|
||||||
|
|
||||||
|
var out io.Writer
|
||||||
|
if flags.output == "-" {
|
||||||
|
out = cmd.OutOrStdout()
|
||||||
|
} else {
|
||||||
|
f, err := os.Create(flags.output)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating output file %q: %w", flags.output, err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
out = f
|
||||||
|
}
|
||||||
|
|
||||||
|
digest, err := extract.Digest(index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("OCI image digest: %s", digest)
|
||||||
|
|
||||||
|
if err := inject.Render(out, inject.PinningValues{
|
||||||
|
Package: flags.pkg,
|
||||||
|
Ident: flags.identifier,
|
||||||
|
Registry: flags.imageRegistry,
|
||||||
|
Prefix: flags.imagePrefix,
|
||||||
|
Name: flags.imageName,
|
||||||
|
Tag: flags.imageTag,
|
||||||
|
Digest: digest,
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("rendering Go code: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Go code created at %q 🤖", flags.output)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type codegenFlags struct {
|
||||||
|
ociPath string
|
||||||
|
output string
|
||||||
|
pkg string
|
||||||
|
identifier string
|
||||||
|
imageRegistry string
|
||||||
|
imagePrefix string
|
||||||
|
imageName string
|
||||||
|
imageTag string
|
||||||
|
logLevel zapcore.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCodegenFlags(cmd *cobra.Command) (codegenFlags, error) {
|
||||||
|
ociPath, err := cmd.Flags().GetString("oci-path")
|
||||||
|
if err != nil {
|
||||||
|
return codegenFlags{}, err
|
||||||
|
}
|
||||||
|
output, err := cmd.Flags().GetString("output")
|
||||||
|
if err != nil {
|
||||||
|
return codegenFlags{}, err
|
||||||
|
}
|
||||||
|
pkg, err := cmd.Flags().GetString("package")
|
||||||
|
if err != nil {
|
||||||
|
return codegenFlags{}, err
|
||||||
|
}
|
||||||
|
identifier, err := cmd.Flags().GetString("identifier")
|
||||||
|
if err != nil {
|
||||||
|
return codegenFlags{}, err
|
||||||
|
}
|
||||||
|
imageRegistry, err := cmd.Flags().GetString("image-registry")
|
||||||
|
if err != nil {
|
||||||
|
return codegenFlags{}, err
|
||||||
|
}
|
||||||
|
imagePrefix, err := cmd.Flags().GetString("image-prefix")
|
||||||
|
if err != nil {
|
||||||
|
return codegenFlags{}, err
|
||||||
|
}
|
||||||
|
imageName, err := cmd.Flags().GetString("image-name")
|
||||||
|
if err != nil {
|
||||||
|
return codegenFlags{}, err
|
||||||
|
}
|
||||||
|
imageTag, err := cmd.Flags().GetString("image-tag")
|
||||||
|
if err != nil {
|
||||||
|
return codegenFlags{}, err
|
||||||
|
}
|
||||||
|
imageTagFile, err := cmd.Flags().GetString("image-tag-file")
|
||||||
|
if err != nil {
|
||||||
|
return codegenFlags{}, err
|
||||||
|
}
|
||||||
|
if imageTagFile != "" {
|
||||||
|
tag, err := os.ReadFile(imageTagFile)
|
||||||
|
if err != nil {
|
||||||
|
return codegenFlags{}, fmt.Errorf("reading image tag file %q: %w", imageTagFile, err)
|
||||||
|
}
|
||||||
|
imageTag = strings.TrimSpace(string(tag))
|
||||||
|
}
|
||||||
|
verbose, err := cmd.Flags().GetBool("verbose")
|
||||||
|
if err != nil {
|
||||||
|
return codegenFlags{}, err
|
||||||
|
}
|
||||||
|
logLevel := zapcore.InfoLevel
|
||||||
|
if verbose {
|
||||||
|
logLevel = zapcore.DebugLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
return codegenFlags{
|
||||||
|
ociPath: ociPath,
|
||||||
|
output: output,
|
||||||
|
pkg: pkg,
|
||||||
|
identifier: identifier,
|
||||||
|
imageRegistry: imageRegistry,
|
||||||
|
imagePrefix: imagePrefix,
|
||||||
|
imageName: imageName,
|
||||||
|
imageTag: imageTag,
|
||||||
|
logLevel: logLevel,
|
||||||
|
}, nil
|
||||||
|
}
|
19
hack/oci-pin/internal/extract/BUILD.bazel
Normal file
19
hack/oci-pin/internal/extract/BUILD.bazel
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||||
|
load("//bazel/go:go_test.bzl", "go_test")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "extract",
|
||||||
|
srcs = ["extract.go"],
|
||||||
|
importpath = "github.com/edgelesssys/constellation/v2/hack/oci-pin/internal/extract",
|
||||||
|
visibility = ["//hack/oci-pin:__subpackages__"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "extract_test",
|
||||||
|
srcs = ["extract_test.go"],
|
||||||
|
embed = [":extract"],
|
||||||
|
deps = [
|
||||||
|
"@com_github_stretchr_testify//assert",
|
||||||
|
"@com_github_stretchr_testify//require",
|
||||||
|
],
|
||||||
|
)
|
52
hack/oci-pin/internal/extract/extract.go
Normal file
52
hack/oci-pin/internal/extract/extract.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package extract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var digestRegexp = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
||||||
|
|
||||||
|
const (
|
||||||
|
supportedSchemaVersion = 2
|
||||||
|
supportedMediaType = "application/vnd.oci.image.index.v1+json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Digest extracts the digest from an OCI index.
|
||||||
|
func Digest(index io.Reader) (string, error) {
|
||||||
|
var oci ociIndex
|
||||||
|
if err := json.NewDecoder(index).Decode(&oci); err != nil {
|
||||||
|
return "", fmt.Errorf("decoding oci index: %w", err)
|
||||||
|
}
|
||||||
|
if oci.SchemaVersion != supportedSchemaVersion {
|
||||||
|
return "", fmt.Errorf("unsupported schema version %d", oci.SchemaVersion)
|
||||||
|
}
|
||||||
|
if oci.MediaType != supportedMediaType {
|
||||||
|
return "", fmt.Errorf("unsupported media type %q", oci.MediaType)
|
||||||
|
}
|
||||||
|
if len(oci.Manifests) != 1 {
|
||||||
|
return "", fmt.Errorf("expected 1 manifest, got %d", len(oci.Manifests))
|
||||||
|
}
|
||||||
|
digest := oci.Manifests[0].Digest
|
||||||
|
if matched := digestRegexp.MatchString(digest); !matched {
|
||||||
|
return "", fmt.Errorf("malformed digest %q", digest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return digest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ociIndex struct {
|
||||||
|
SchemaVersion int `json:"schemaVersion"`
|
||||||
|
MediaType string `json:"mediaType"`
|
||||||
|
Manifests []struct {
|
||||||
|
MediaType string `json:"mediaType"`
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
} `json:"manifests"`
|
||||||
|
}
|
116
hack/oci-pin/internal/extract/extract_test.go
Normal file
116
hack/oci-pin/internal/extract/extract_test.go
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package extract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDigest(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
ociIndex string
|
||||||
|
wantDigest string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
"valid OCI index": {
|
||||||
|
ociIndex: validOCIIndex,
|
||||||
|
wantDigest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
"wrong version": {
|
||||||
|
ociIndex: `{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||||
|
"manifests": [
|
||||||
|
{
|
||||||
|
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||||
|
"digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"size": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"wrong media type": {
|
||||||
|
ociIndex: `{
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"mediaType": "application/something-else",
|
||||||
|
"manifests": [
|
||||||
|
{
|
||||||
|
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||||
|
"digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"size": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"incorrect manifest length": {
|
||||||
|
ociIndex: `{
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||||
|
"manifests": []
|
||||||
|
}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"incorrect manifest digest format": {
|
||||||
|
ociIndex: `{
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||||
|
"manifests": [
|
||||||
|
{
|
||||||
|
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||||
|
"digest": "foo:bar",
|
||||||
|
"size": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"malformed json": {
|
||||||
|
ociIndex: `}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
require := require.New(t)
|
||||||
|
digest, err := Digest(bytes.NewBufferString(tc.ociIndex))
|
||||||
|
if tc.wantErr {
|
||||||
|
require.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(err)
|
||||||
|
assert.Equal(tc.wantDigest, digest)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// This is a valid OCI index.
|
||||||
|
// It has a schema version of 2, a media type of
|
||||||
|
// "application/vnd.oci.image.index.v1+json", and a single manifest.
|
||||||
|
// The manifest has a media type of
|
||||||
|
// "application/vnd.oci.image.manifest.v1+json", and a digest.
|
||||||
|
// The digest is a valid SHA256 hash.
|
||||||
|
validOCIIndex = `{
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||||
|
"manifests": [
|
||||||
|
{
|
||||||
|
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||||
|
"digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"size": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
)
|
19
hack/oci-pin/internal/inject/BUILD.bazel
Normal file
19
hack/oci-pin/internal/inject/BUILD.bazel
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||||
|
load("//bazel/go:go_test.bzl", "go_test")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "inject",
|
||||||
|
srcs = ["inject.go"],
|
||||||
|
importpath = "github.com/edgelesssys/constellation/v2/hack/oci-pin/internal/inject",
|
||||||
|
visibility = ["//hack/oci-pin:__subpackages__"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "inject_test",
|
||||||
|
srcs = ["inject_test.go"],
|
||||||
|
embed = [":inject"],
|
||||||
|
deps = [
|
||||||
|
"@com_github_stretchr_testify//assert",
|
||||||
|
"@com_github_stretchr_testify//require",
|
||||||
|
],
|
||||||
|
)
|
130
hack/oci-pin/internal/inject/inject.go
Normal file
130
hack/oci-pin/internal/inject/inject.go
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
// inject renders Go source files with injected pinning values.
|
||||||
|
package inject
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Render renders the source code to inject the pinned values.
|
||||||
|
func Render(out io.Writer, vals PinningValues) error {
|
||||||
|
if err := validate(vals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sourceTpl.Execute(out, vals)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate validates the values to inject.
|
||||||
|
func validate(vals PinningValues) error {
|
||||||
|
if vals.Package == "" {
|
||||||
|
return errors.New("package is empty")
|
||||||
|
}
|
||||||
|
if vals.Ident == "" {
|
||||||
|
return errors.New("identifier is empty")
|
||||||
|
}
|
||||||
|
if vals.Registry == "" {
|
||||||
|
return errors.New("registry is empty")
|
||||||
|
}
|
||||||
|
if vals.Name == "" {
|
||||||
|
return errors.New("name is empty")
|
||||||
|
}
|
||||||
|
if vals.Digest == "" {
|
||||||
|
return errors.New("digest is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched := packageNameRegexp.MatchString(vals.Package); !matched {
|
||||||
|
return errors.New("package is not valid")
|
||||||
|
}
|
||||||
|
if matched := identRegexp.MatchString(vals.Ident); !matched {
|
||||||
|
return errors.New("identifier is not valid")
|
||||||
|
}
|
||||||
|
if matched := registryRegexp.MatchString(vals.Registry); !matched {
|
||||||
|
return errors.New("registry is not valid")
|
||||||
|
}
|
||||||
|
if matched := prefixRegexp.MatchString(vals.Prefix); !matched {
|
||||||
|
return errors.New("prefix is not valid")
|
||||||
|
}
|
||||||
|
if matched := nameRegexp.MatchString(vals.Name); !matched {
|
||||||
|
return errors.New("name is not valid")
|
||||||
|
}
|
||||||
|
if matched := tagRegexp.MatchString(vals.Tag); vals.Tag != "" && !matched {
|
||||||
|
return errors.New("tag is not valid")
|
||||||
|
}
|
||||||
|
if matched := digestRegexp.MatchString(vals.Digest); !matched {
|
||||||
|
return errors.New("digest is not valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PinningValues contains the values to inject into the generated source code.
|
||||||
|
type PinningValues struct {
|
||||||
|
// Package is the name of the package to generate.
|
||||||
|
Package string
|
||||||
|
// Ident is the base identifier of the generated constants.
|
||||||
|
Ident string
|
||||||
|
// Registry string
|
||||||
|
Registry string
|
||||||
|
// Prefix is the prefix of the container image name.
|
||||||
|
Prefix string
|
||||||
|
// Name is the name of the container image.
|
||||||
|
Name string
|
||||||
|
// Tag is the (optional) tag of the container image.
|
||||||
|
Tag string
|
||||||
|
// Digest is the digest of the container image.
|
||||||
|
Digest string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sourceTpl = template.Must(template.New("goSource").Parse(goSourceTpl))
|
||||||
|
|
||||||
|
// packageNameRegexp is the regular expression for a valid package name.
|
||||||
|
packageNameRegexp = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
|
||||||
|
|
||||||
|
// identRegexp is the regular expression for a valid identifier.
|
||||||
|
identRegexp = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||||
|
|
||||||
|
// registryRegexp is the regular expression for a valid registry.
|
||||||
|
registryRegexp = regexp.MustCompile(`^[^\s/"]+$`)
|
||||||
|
|
||||||
|
// prefixRegexp is the regular expression for a valid prefix.
|
||||||
|
prefixRegexp = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9-_/]*)?$`)
|
||||||
|
|
||||||
|
// nameRegexp is the regular expression for a valid name.
|
||||||
|
nameRegexp = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-_]*$`)
|
||||||
|
|
||||||
|
// tagRegexp is the regular expression for a valid tag.
|
||||||
|
tagRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
|
||||||
|
|
||||||
|
// digestRegexp is the regular expression for a valid digest.
|
||||||
|
digestRegexp = regexp.MustCompile(`^sha256:[a-f0-9]{64}$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
const goSourceTpl = `package {{.Package}}
|
||||||
|
|
||||||
|
// Code generated by oci-pin. DO NOT EDIT.
|
||||||
|
|
||||||
|
const (
|
||||||
|
// {{.Ident}}Registry is the {{.Name}} container image registry.
|
||||||
|
{{.Ident}}Registry = "{{.Registry}}"
|
||||||
|
|
||||||
|
{{if .Prefix }} // {{.Ident}}Prefix is the {{.Name}} container image prefix.
|
||||||
|
{{.Ident}}Prefix = "{{.Prefix}}"
|
||||||
|
|
||||||
|
{{end}} // {{.Ident}}Name is the {{.Name}} container image short name part.
|
||||||
|
{{.Ident}}Name = "{{.Name}}"
|
||||||
|
|
||||||
|
{{if .Tag }} // {{.Ident}}Tag is the tag for the {{.Name}} container image.
|
||||||
|
{{.Ident}}Tag = "{{.Tag}}"
|
||||||
|
|
||||||
|
{{end}} // {{.Ident}}Digest is the digest for the {{.Name}} container image.
|
||||||
|
{{.Ident}}Digest = "{{.Digest}}"
|
||||||
|
)
|
||||||
|
`
|
201
hack/oci-pin/internal/inject/inject_test.go
Normal file
201
hack/oci-pin/internal/inject/inject_test.go
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package inject
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRender(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
modifier func(vals *PinningValues)
|
||||||
|
wantTemplate string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
"valid": {
|
||||||
|
wantTemplate: `package foo
|
||||||
|
|
||||||
|
// Code generated by oci-pin. DO NOT EDIT.
|
||||||
|
|
||||||
|
const (
|
||||||
|
// barServiceRegistry is the bar-service container image registry.
|
||||||
|
barServiceRegistry = "registry.example.com"
|
||||||
|
|
||||||
|
// barServicePrefix is the bar-service container image prefix.
|
||||||
|
barServicePrefix = "staging"
|
||||||
|
|
||||||
|
// barServiceName is the bar-service container image short name part.
|
||||||
|
barServiceName = "bar-service"
|
||||||
|
|
||||||
|
// barServiceTag is the tag for the bar-service container image.
|
||||||
|
barServiceTag = "v1.2.3"
|
||||||
|
|
||||||
|
// barServiceDigest is the digest for the bar-service container image.
|
||||||
|
barServiceDigest = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
"valid without prefix": {
|
||||||
|
modifier: func(vals *PinningValues) {
|
||||||
|
vals.Prefix = ""
|
||||||
|
},
|
||||||
|
wantTemplate: `package foo
|
||||||
|
|
||||||
|
// Code generated by oci-pin. DO NOT EDIT.
|
||||||
|
|
||||||
|
const (
|
||||||
|
// barServiceRegistry is the bar-service container image registry.
|
||||||
|
barServiceRegistry = "registry.example.com"
|
||||||
|
|
||||||
|
// barServiceName is the bar-service container image short name part.
|
||||||
|
barServiceName = "bar-service"
|
||||||
|
|
||||||
|
// barServiceTag is the tag for the bar-service container image.
|
||||||
|
barServiceTag = "v1.2.3"
|
||||||
|
|
||||||
|
// barServiceDigest is the digest for the bar-service container image.
|
||||||
|
barServiceDigest = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
"valid without tag": {
|
||||||
|
modifier: func(vals *PinningValues) {
|
||||||
|
vals.Tag = ""
|
||||||
|
},
|
||||||
|
wantTemplate: `package foo
|
||||||
|
|
||||||
|
// Code generated by oci-pin. DO NOT EDIT.
|
||||||
|
|
||||||
|
const (
|
||||||
|
// barServiceRegistry is the bar-service container image registry.
|
||||||
|
barServiceRegistry = "registry.example.com"
|
||||||
|
|
||||||
|
// barServicePrefix is the bar-service container image prefix.
|
||||||
|
barServicePrefix = "staging"
|
||||||
|
|
||||||
|
// barServiceName is the bar-service container image short name part.
|
||||||
|
barServiceName = "bar-service"
|
||||||
|
|
||||||
|
// barServiceDigest is the digest for the bar-service container image.
|
||||||
|
barServiceDigest = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
"missing package": {
|
||||||
|
modifier: func(vals *PinningValues) {
|
||||||
|
vals.Package = ""
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"missing ident": {
|
||||||
|
modifier: func(vals *PinningValues) {
|
||||||
|
vals.Ident = ""
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"missing registry": {
|
||||||
|
modifier: func(vals *PinningValues) {
|
||||||
|
vals.Registry = ""
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"missing name": {
|
||||||
|
modifier: func(vals *PinningValues) {
|
||||||
|
vals.Name = ""
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"missing digest": {
|
||||||
|
modifier: func(vals *PinningValues) {
|
||||||
|
vals.Digest = ""
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"malformed package": {
|
||||||
|
modifier: func(vals *PinningValues) {
|
||||||
|
vals.Package = "foo bar"
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"malformed ident": {
|
||||||
|
modifier: func(vals *PinningValues) {
|
||||||
|
vals.Ident = "foo bar"
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"malformed registry": {
|
||||||
|
modifier: func(vals *PinningValues) {
|
||||||
|
vals.Registry = "\n"
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"malformed prefix": {
|
||||||
|
modifier: func(vals *PinningValues) {
|
||||||
|
vals.Prefix = "\n"
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"malformed name": {
|
||||||
|
modifier: func(vals *PinningValues) {
|
||||||
|
vals.Name = "foo bar"
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"malformed tag": {
|
||||||
|
modifier: func(vals *PinningValues) {
|
||||||
|
vals.Tag = `"`
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"malformed digest": {
|
||||||
|
modifier: func(vals *PinningValues) {
|
||||||
|
vals.Digest = "foo bar"
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
vals := defaultPinningValues()
|
||||||
|
|
||||||
|
if tc.modifier != nil {
|
||||||
|
tc.modifier(vals)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := Render(&buf, *vals)
|
||||||
|
if tc.wantErr {
|
||||||
|
require.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(err)
|
||||||
|
assert.Equal(tc.wantTemplate, buf.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultPinningValues() *PinningValues {
|
||||||
|
return &PinningValues{
|
||||||
|
Package: "foo",
|
||||||
|
Ident: "barService",
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "bar-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
}
|
||||||
|
}
|
19
hack/oci-pin/internal/sums/BUILD.bazel
Normal file
19
hack/oci-pin/internal/sums/BUILD.bazel
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||||
|
load("//bazel/go:go_test.bzl", "go_test")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "sums",
|
||||||
|
srcs = ["sums.go"],
|
||||||
|
importpath = "github.com/edgelesssys/constellation/v2/hack/oci-pin/internal/sums",
|
||||||
|
visibility = ["//hack/oci-pin:__subpackages__"],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "sums_test",
|
||||||
|
srcs = ["sums_test.go"],
|
||||||
|
embed = [":sums"],
|
||||||
|
deps = [
|
||||||
|
"@com_github_stretchr_testify//assert",
|
||||||
|
"@com_github_stretchr_testify//require",
|
||||||
|
],
|
||||||
|
)
|
227
hack/oci-pin/internal/sums/sums.go
Normal file
227
hack/oci-pin/internal/sums/sums.go
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
// sums creates and combines sha256sums files.
|
||||||
|
package sums
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var sumRegexp = regexp.MustCompile(`^[a-f0-9]{64}$`)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sumLength = 64
|
||||||
|
minLineLength = sumLength + 2 // 64 hex + 2 space
|
||||||
|
notFound = -1 // index not found
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create creates a sha256sums file from a list of digests.
|
||||||
|
// The digests going in are expected to be in the oci format "sha256:hex".
|
||||||
|
func Create(refs []PinnedImageReference, out io.Writer) error {
|
||||||
|
coalescedRefs, err := coalesceRefs(refs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, ref := range coalescedRefs {
|
||||||
|
if _, err := fmt.Fprintf(out, "%s %s\n", ref.Sum(), ref.Reference()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges multiple sha256sums files into one.
|
||||||
|
func Merge(refs [][]PinnedImageReference, out io.Writer) error {
|
||||||
|
mergedRefs := make([]PinnedImageReference, 0)
|
||||||
|
for _, refs := range refs {
|
||||||
|
mergedRefs = append(mergedRefs, refs...)
|
||||||
|
}
|
||||||
|
return Create(mergedRefs, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses a sha256sums file.
|
||||||
|
func Parse(in io.Reader) ([]PinnedImageReference, error) {
|
||||||
|
scanner := newLineScanner(in)
|
||||||
|
return parseLines(scanner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PinnedImageReference contains the components of a pinned image reference.
|
||||||
|
type PinnedImageReference struct {
|
||||||
|
Registry string
|
||||||
|
Prefix string
|
||||||
|
Name string
|
||||||
|
Tag string
|
||||||
|
Digest string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference returns the string representation of the reference (without the digest).
|
||||||
|
func (r PinnedImageReference) Reference() string {
|
||||||
|
var base string
|
||||||
|
if r.Prefix == "" {
|
||||||
|
base = path.Join(r.Registry, r.Name)
|
||||||
|
} else {
|
||||||
|
base = path.Join(r.Registry, r.Prefix, r.Name)
|
||||||
|
}
|
||||||
|
var tag string
|
||||||
|
if r.Tag != "" {
|
||||||
|
tag = ":" + r.Tag
|
||||||
|
}
|
||||||
|
return base + tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum returns the string representation of the digest.
|
||||||
|
// The digest is expected to be in the oci format "sha256:hex".
|
||||||
|
// The resulting Sum is only the hex part.
|
||||||
|
func (r PinnedImageReference) Sum() string {
|
||||||
|
return r.Digest[len("sha256:"):]
|
||||||
|
}
|
||||||
|
|
||||||
|
// coalesceRefs coalesces the image references.
|
||||||
|
// It sorts the references and removes duplicates.
|
||||||
|
// If conflicting digests are found, an error is returned.
|
||||||
|
func coalesceRefs(refs []PinnedImageReference) ([]PinnedImageReference, error) {
|
||||||
|
sortRefs(refs)
|
||||||
|
uniqueRefs := make([]PinnedImageReference, 0, len(refs))
|
||||||
|
var prev PinnedImageReference
|
||||||
|
for _, ref := range refs {
|
||||||
|
equal, err := compareRefs(prev, ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !equal {
|
||||||
|
uniqueRefs = append(uniqueRefs, ref)
|
||||||
|
prev = ref
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueRefs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortRefs(refs []PinnedImageReference) {
|
||||||
|
sort.Slice(refs, func(i, j int) bool {
|
||||||
|
if refs[i].Registry != refs[j].Registry {
|
||||||
|
return refs[i].Registry < refs[j].Registry
|
||||||
|
}
|
||||||
|
if refs[i].Prefix != refs[j].Prefix {
|
||||||
|
return refs[i].Prefix < refs[j].Prefix
|
||||||
|
}
|
||||||
|
if refs[i].Name != refs[j].Name {
|
||||||
|
return refs[i].Name < refs[j].Name
|
||||||
|
}
|
||||||
|
if refs[i].Tag != refs[j].Tag {
|
||||||
|
return refs[i].Tag < refs[j].Tag
|
||||||
|
}
|
||||||
|
return refs[i].Digest < refs[j].Digest
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareRefs compares two references.
|
||||||
|
// references are equal if they have the same registry, prefix, name, tag and digest.
|
||||||
|
// If refs only differ in digest, they are inconsistent and an error is returned.
|
||||||
|
func compareRefs(a, b PinnedImageReference) (equal bool, err error) {
|
||||||
|
if a.Registry != b.Registry {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if a.Prefix != b.Prefix {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if a.Name != b.Name {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if a.Tag != b.Tag {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if a.Digest != b.Digest {
|
||||||
|
return false, errors.New("conflicting digests")
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLines(scanner scanner) ([]PinnedImageReference, error) {
|
||||||
|
var refs []PinnedImageReference
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ref, err := parseLine(line)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
refs = append(refs, ref)
|
||||||
|
}
|
||||||
|
return refs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLine parses a line from a sha256sums file.
|
||||||
|
// The line is expected to be in the format "hex reference".
|
||||||
|
func parseLine(line string) (PinnedImageReference, error) {
|
||||||
|
parts := strings.Split(line, " ")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return PinnedImageReference{}, fmt.Errorf("line does not have exactly 2 parts separated by two spaces: %q", line)
|
||||||
|
}
|
||||||
|
return refFromSumAndRef(parts[0], parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func refFromSumAndRef(sum, ref string) (PinnedImageReference, error) {
|
||||||
|
if !sumRegexp.MatchString(sum) {
|
||||||
|
return PinnedImageReference{}, fmt.Errorf("invalid sum: %q", sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// last colon is separator between name and tag
|
||||||
|
var base, tag string
|
||||||
|
tagSep := strings.LastIndexByte(ref, ':')
|
||||||
|
if tagSep == notFound {
|
||||||
|
base = ref
|
||||||
|
} else {
|
||||||
|
base = ref[:tagSep]
|
||||||
|
tag = ref[tagSep+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// first slash is separator between registry and full name
|
||||||
|
registrySep := strings.IndexByte(base, '/')
|
||||||
|
if registrySep == notFound {
|
||||||
|
return PinnedImageReference{}, fmt.Errorf("invalid reference: missing registry %q", ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
registry := base[:registrySep]
|
||||||
|
fullName := base[registrySep+1:]
|
||||||
|
|
||||||
|
// last slash is separator between prefix and short name
|
||||||
|
var prefix, name string
|
||||||
|
nameSep := strings.LastIndexByte(fullName, '/')
|
||||||
|
if nameSep == notFound {
|
||||||
|
name = fullName
|
||||||
|
} else {
|
||||||
|
prefix = fullName[:nameSep]
|
||||||
|
name = fullName[nameSep+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return PinnedImageReference{
|
||||||
|
Registry: registry,
|
||||||
|
Prefix: prefix,
|
||||||
|
Name: name,
|
||||||
|
Tag: tag,
|
||||||
|
Digest: "sha256:" + sum,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLineScanner(r io.Reader) scanner {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
scanner.Split(bufio.ScanLines)
|
||||||
|
return scanner
|
||||||
|
}
|
||||||
|
|
||||||
|
type scanner interface {
|
||||||
|
Scan() bool
|
||||||
|
Text() string
|
||||||
|
}
|
337
hack/oci-pin/internal/sums/sums_test.go
Normal file
337
hack/oci-pin/internal/sums/sums_test.go
Normal file
|
@ -0,0 +1,337 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sums
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateParse(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
refs []PinnedImageReference
|
||||||
|
wantErr bool
|
||||||
|
wantOut string
|
||||||
|
wantRefs []PinnedImageReference
|
||||||
|
}{
|
||||||
|
"single image": {
|
||||||
|
refs: []PinnedImageReference{
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOut: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 registry.example.com/staging/foo-service:v1.2.3\n",
|
||||||
|
},
|
||||||
|
"no prefix": {
|
||||||
|
refs: []PinnedImageReference{
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOut: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 registry.example.com/foo-service:v1.2.3\n",
|
||||||
|
},
|
||||||
|
"no tag": {
|
||||||
|
refs: []PinnedImageReference{
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOut: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 registry.example.com/staging/foo-service\n",
|
||||||
|
},
|
||||||
|
"multiple images": {
|
||||||
|
refs: []PinnedImageReference{
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "bar-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "production",
|
||||||
|
Name: "baz-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v2.0.0",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Registry: "backup.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantRefs: []PinnedImageReference{
|
||||||
|
{
|
||||||
|
Registry: "backup.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "production",
|
||||||
|
Name: "baz-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "bar-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v2.0.0",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOut: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 backup.example.com/staging/foo-service:v1.2.3\n" +
|
||||||
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 registry.example.com/production/baz-service:v1.2.3\n" +
|
||||||
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 registry.example.com/staging/bar-service:v1.2.3\n" +
|
||||||
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 registry.example.com/staging/foo-service:v1.2.3\n" +
|
||||||
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 registry.example.com/staging/foo-service:v2.0.0\n",
|
||||||
|
},
|
||||||
|
"duplicate images": {
|
||||||
|
refs: []PinnedImageReference{
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantRefs: []PinnedImageReference{
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOut: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 registry.example.com/staging/foo-service:v1.2.3\n",
|
||||||
|
},
|
||||||
|
"duplicate images with different hashes": {
|
||||||
|
refs: []PinnedImageReference{
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name+"_create", func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := Create(tc.refs, &buf)
|
||||||
|
if tc.wantErr {
|
||||||
|
require.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(err)
|
||||||
|
assert.Equal(tc.wantOut, buf.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
if tc.wantErr {
|
||||||
|
continue // skip inverse test cases where the forward test case is expected to fail
|
||||||
|
}
|
||||||
|
t.Run(name+"_parse", func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
buf := bytes.NewBufferString(tc.wantOut)
|
||||||
|
gotRefs, err := Parse(buf)
|
||||||
|
if tc.wantErr {
|
||||||
|
require.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(err)
|
||||||
|
wantRefs := tc.refs
|
||||||
|
if len(tc.wantRefs) > 0 {
|
||||||
|
wantRefs = tc.wantRefs
|
||||||
|
}
|
||||||
|
assert.Equal(wantRefs, gotRefs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMerge(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
refs [][]PinnedImageReference
|
||||||
|
wantErr bool
|
||||||
|
wantOut string
|
||||||
|
}{
|
||||||
|
"different images": {
|
||||||
|
refs: [][]PinnedImageReference{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "bar-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOut: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 registry.example.com/staging/bar-service:v1.2.3\n" + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 registry.example.com/staging/foo-service:v1.2.3\n",
|
||||||
|
},
|
||||||
|
"duplicate images": {
|
||||||
|
refs: [][]PinnedImageReference{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Registry: "registry.example.com",
|
||||||
|
Prefix: "staging",
|
||||||
|
Name: "foo-service",
|
||||||
|
Tag: "v1.2.3",
|
||||||
|
Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantOut: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 registry.example.com/staging/foo-service:v1.2.3\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := Merge(tc.refs, &buf)
|
||||||
|
if tc.wantErr {
|
||||||
|
require.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(err)
|
||||||
|
assert.Equal(tc.wantOut, buf.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParse has additional test cases that are not covered by TestCreateParse.
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
testCases := map[string]struct {
|
||||||
|
in string
|
||||||
|
wantErr bool
|
||||||
|
wantRefs []PinnedImageReference
|
||||||
|
}{
|
||||||
|
"empty line": {
|
||||||
|
in: "\n\n",
|
||||||
|
},
|
||||||
|
"line too short": {
|
||||||
|
in: "short",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"malformed digest": {
|
||||||
|
in: "malformed-digest-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa registry.example.com/staging/foo-service:v1.2.3",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"missing registry": {
|
||||||
|
in: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 foo-service:v1.2.3",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
buf := bytes.NewBufferString(tc.in)
|
||||||
|
gotRefs, err := Parse(buf)
|
||||||
|
if tc.wantErr {
|
||||||
|
require.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(err)
|
||||||
|
assert.Equal(tc.wantRefs, gotRefs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
122
hack/oci-pin/merge.go
Normal file
122
hack/oci-pin/merge.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/hack/oci-pin/internal/sums"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newMergeCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "merge",
|
||||||
|
Short: "Merge multiple sha256sum files that pin OCI images.",
|
||||||
|
RunE: runMerge,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringArray("input", nil, "Path to existing sha256sum file that should be merged.")
|
||||||
|
cmd.Flags().String("output", "-", "Output file. If not set, the output is written to stdout.")
|
||||||
|
must(cmd.MarkFlagRequired("input"))
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMerge(cmd *cobra.Command, _ []string) error {
|
||||||
|
flags, err := parseMergeFlags(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log := logger.New(logger.PlainLog, flags.logLevel)
|
||||||
|
log.Debugf("Parsed flags: %+v", flags)
|
||||||
|
|
||||||
|
log.Debugf("Merging sum file from %q into %q.", flags.inputs, flags.output)
|
||||||
|
|
||||||
|
var out io.Writer
|
||||||
|
if flags.output == "-" {
|
||||||
|
out = cmd.OutOrStdout()
|
||||||
|
} else {
|
||||||
|
f, err := os.Create(flags.output)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating output file %q: %w", flags.output, err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
out = f
|
||||||
|
}
|
||||||
|
|
||||||
|
unmergedRefs, err := parseInputs(flags.inputs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading input files: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sums.Merge(unmergedRefs, out); err != nil {
|
||||||
|
return fmt.Errorf("creating merged sum file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Sum file created at %q 🤖", flags.output)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInputs(inputs []string) ([][]sums.PinnedImageReference, error) {
|
||||||
|
var unmergedRefs [][]sums.PinnedImageReference
|
||||||
|
for _, input := range inputs {
|
||||||
|
refs, err := parseInput(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
unmergedRefs = append(unmergedRefs, refs)
|
||||||
|
}
|
||||||
|
return unmergedRefs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInput(input string) ([]sums.PinnedImageReference, error) {
|
||||||
|
in, err := os.Open(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("opening sum file at %q: %w", input, err)
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
refs, err := sums.Parse(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing sums %q: %w", input, err)
|
||||||
|
}
|
||||||
|
return refs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mergeFlags struct {
|
||||||
|
inputs []string
|
||||||
|
output string
|
||||||
|
logLevel zapcore.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMergeFlags(cmd *cobra.Command) (mergeFlags, error) {
|
||||||
|
inputs, err := cmd.Flags().GetStringArray("input")
|
||||||
|
if err != nil {
|
||||||
|
return mergeFlags{}, err
|
||||||
|
}
|
||||||
|
output, err := cmd.Flags().GetString("output")
|
||||||
|
if err != nil {
|
||||||
|
return mergeFlags{}, err
|
||||||
|
}
|
||||||
|
verbose, err := cmd.Flags().GetBool("verbose")
|
||||||
|
if err != nil {
|
||||||
|
return mergeFlags{}, err
|
||||||
|
}
|
||||||
|
logLevel := zapcore.InfoLevel
|
||||||
|
if verbose {
|
||||||
|
logLevel = zapcore.DebugLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergeFlags{
|
||||||
|
inputs: inputs,
|
||||||
|
output: output,
|
||||||
|
logLevel: logLevel,
|
||||||
|
}, nil
|
||||||
|
}
|
85
hack/oci-pin/oci-pin.go
Normal file
85
hack/oci-pin/oci-pin.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
// oci-pin generates Go code and shasum files for OCI images.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func execute() error {
|
||||||
|
rootCmd := newRootCmd()
|
||||||
|
ctx, cancel := signalContext(context.Background(), os.Interrupt)
|
||||||
|
defer cancel()
|
||||||
|
return rootCmd.ExecuteContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRootCmd() *cobra.Command {
|
||||||
|
rootCmd := &cobra.Command{
|
||||||
|
Use: "oci-pin",
|
||||||
|
Short: "Generate pinning artifacts for OCI images.",
|
||||||
|
Long: "Generate pinning artifacts (Go code, shasum files) for OCI images.",
|
||||||
|
PersistentPreRun: preRunRoot,
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCmd.SetOut(os.Stdout)
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().Bool("verbose", false, "Enable verbose output.")
|
||||||
|
|
||||||
|
rootCmd.AddCommand(newCodegenCmd())
|
||||||
|
rootCmd.AddCommand(newSumCmd())
|
||||||
|
rootCmd.AddCommand(newMergeCmd())
|
||||||
|
|
||||||
|
return rootCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// signalContext returns a context that is canceled on the handed signal.
|
||||||
|
// The signal isn't watched after its first occurrence. Call the cancel
|
||||||
|
// function to ensure the internal goroutine is stopped and the signal isn't
|
||||||
|
// watched any longer.
|
||||||
|
func signalContext(ctx context.Context, sig os.Signal) (context.Context, context.CancelFunc) {
|
||||||
|
sigCtx, stop := signal.NotifyContext(ctx, sig)
|
||||||
|
done := make(chan struct{}, 1)
|
||||||
|
stopDone := make(chan struct{}, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer func() { stopDone <- struct{}{} }()
|
||||||
|
defer stop()
|
||||||
|
select {
|
||||||
|
case <-sigCtx.Done():
|
||||||
|
fmt.Println(" Signal caught. Press ctrl+c again to terminate the program immediately.")
|
||||||
|
case <-done:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
cancelFunc := func() {
|
||||||
|
done <- struct{}{}
|
||||||
|
<-stopDone
|
||||||
|
}
|
||||||
|
|
||||||
|
return sigCtx, cancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func preRunRoot(cmd *cobra.Command, _ []string) {
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func must(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
162
hack/oci-pin/sum.go
Normal file
162
hack/oci-pin/sum.go
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/hack/oci-pin/internal/extract"
|
||||||
|
"github.com/edgelesssys/constellation/v2/hack/oci-pin/internal/sums"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newSumCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "sum",
|
||||||
|
Short: "Generate sha256sum file that pins an OCI image.",
|
||||||
|
RunE: runSum,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().String("oci-path", "", "Path to the OCI image to pin.")
|
||||||
|
cmd.Flags().String("output", "-", "Output file. If not set, the output is written to stdout.")
|
||||||
|
cmd.Flags().String("registry", "", "OCI registry to use.")
|
||||||
|
cmd.Flags().String("prefix", "", "Prefix of the OCI image to pin.")
|
||||||
|
cmd.Flags().String("image-name", "", "Short name (suffix) of the OCI image to pin.")
|
||||||
|
cmd.Flags().String("image-tag", "", "Tag of the OCI image to pin. Optional.")
|
||||||
|
cmd.Flags().String("image-tag-file", "", "Tag file of the OCI image to pin. Optional.")
|
||||||
|
cmd.MarkFlagsMutuallyExclusive("image-tag", "image-tag-file")
|
||||||
|
must(cmd.MarkFlagRequired("registry"))
|
||||||
|
must(cmd.MarkFlagRequired("oci-path"))
|
||||||
|
must(cmd.MarkFlagRequired("image-name"))
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSum(cmd *cobra.Command, _ []string) error {
|
||||||
|
flags, err := parseSumFlags(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log := logger.New(logger.PlainLog, flags.logLevel)
|
||||||
|
log.Debugf("Parsed flags: %+v", flags)
|
||||||
|
|
||||||
|
log.Debugf("Generating sum file for OCI image %s.", flags.imageName)
|
||||||
|
|
||||||
|
ociIndexPath := filepath.Join(flags.ociPath, "index.json")
|
||||||
|
index, err := os.Open(ociIndexPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("opening OCI index at %q: %w", ociIndexPath, err)
|
||||||
|
}
|
||||||
|
defer index.Close()
|
||||||
|
|
||||||
|
var out io.Writer
|
||||||
|
if flags.output == "-" {
|
||||||
|
out = cmd.OutOrStdout()
|
||||||
|
} else {
|
||||||
|
f, err := os.Create(flags.output)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating output file %q: %w", flags.output, err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
out = f
|
||||||
|
}
|
||||||
|
|
||||||
|
digest, err := extract.Digest(index)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("extracting OCI image digest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("OCI image digest: %s", digest)
|
||||||
|
|
||||||
|
refs := []sums.PinnedImageReference{
|
||||||
|
{
|
||||||
|
Registry: flags.registry,
|
||||||
|
Prefix: flags.prefix,
|
||||||
|
Name: flags.imageName,
|
||||||
|
Tag: flags.imageTag,
|
||||||
|
Digest: digest,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sums.Create(refs, out); err != nil {
|
||||||
|
return fmt.Errorf("creating sum file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Sum file created at %q 🤖", flags.output)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type sumFlags struct {
|
||||||
|
ociPath string
|
||||||
|
output string
|
||||||
|
registry string
|
||||||
|
prefix string
|
||||||
|
imageName string
|
||||||
|
imageTag string
|
||||||
|
logLevel zapcore.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSumFlags(cmd *cobra.Command) (sumFlags, error) {
|
||||||
|
ociPath, err := cmd.Flags().GetString("oci-path")
|
||||||
|
if err != nil {
|
||||||
|
return sumFlags{}, err
|
||||||
|
}
|
||||||
|
output, err := cmd.Flags().GetString("output")
|
||||||
|
if err != nil {
|
||||||
|
return sumFlags{}, err
|
||||||
|
}
|
||||||
|
registry, err := cmd.Flags().GetString("registry")
|
||||||
|
if err != nil {
|
||||||
|
return sumFlags{}, err
|
||||||
|
}
|
||||||
|
prefix, err := cmd.Flags().GetString("prefix")
|
||||||
|
if err != nil {
|
||||||
|
return sumFlags{}, err
|
||||||
|
}
|
||||||
|
imageName, err := cmd.Flags().GetString("image-name")
|
||||||
|
if err != nil {
|
||||||
|
return sumFlags{}, err
|
||||||
|
}
|
||||||
|
imageTag, err := cmd.Flags().GetString("image-tag")
|
||||||
|
if err != nil {
|
||||||
|
return sumFlags{}, err
|
||||||
|
}
|
||||||
|
imageTagFile, err := cmd.Flags().GetString("image-tag-file")
|
||||||
|
if err != nil {
|
||||||
|
return sumFlags{}, err
|
||||||
|
}
|
||||||
|
if imageTagFile != "" {
|
||||||
|
tag, err := os.ReadFile(imageTagFile)
|
||||||
|
if err != nil {
|
||||||
|
return sumFlags{}, fmt.Errorf("reading image tag file %q: %w", imageTagFile, err)
|
||||||
|
}
|
||||||
|
imageTag = strings.TrimSpace(string(tag))
|
||||||
|
}
|
||||||
|
verbose, err := cmd.Flags().GetBool("verbose")
|
||||||
|
if err != nil {
|
||||||
|
return sumFlags{}, err
|
||||||
|
}
|
||||||
|
logLevel := zapcore.InfoLevel
|
||||||
|
if verbose {
|
||||||
|
logLevel = zapcore.DebugLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
return sumFlags{
|
||||||
|
ociPath: ociPath,
|
||||||
|
output: output,
|
||||||
|
registry: registry,
|
||||||
|
prefix: prefix,
|
||||||
|
imageName: imageName,
|
||||||
|
imageTag: imageTag,
|
||||||
|
logLevel: logLevel,
|
||||||
|
}, nil
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue