mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-30 09:56:21 -05: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
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…
Reference in New Issue
Block a user