Skip to content

Commit

Permalink
release inspect (#543)
Browse files Browse the repository at this point in the history
Signed-off-by: Brooks Newberry <[email protected]>
  • Loading branch information
brooksn authored Mar 1, 2025
1 parent c97ee67 commit 32c3dab
Show file tree
Hide file tree
Showing 12 changed files with 979 additions and 7 deletions.
1 change: 1 addition & 0 deletions cmd/release/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ release tag system-agent-installer-k3s rc v1.29.2
release tag k3s ga v1.29.2
release tag system-agent-installer-k3s ga v1.29.2
release stats -r rke2 -s 2024-01-01 -e 2024-12-31
release inspect v1.29.2+rke2r1
```

#### Cache Permissions and Docker:
Expand Down
201 changes: 201 additions & 0 deletions cmd/release/cmd/inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package cmd

import (
"context"
"errors"
"fmt"
"io"
"os"
"sort"
"strings"
"text/tabwriter"

"github.com/google/go-containerregistry/pkg/name"
reg "github.com/rancher/ecm-distro-tools/registry"
"github.com/rancher/ecm-distro-tools/release"
"github.com/rancher/ecm-distro-tools/release/rke2"
"github.com/rancher/ecm-distro-tools/repository"
"github.com/spf13/cobra"
)

const (
ossRegistry = "docker.io"
)

func archStatus(expected bool, ossInfo, primeInfo reg.Image, platform reg.Platform) string {
if !expected {
return "-"
}

hasArch := ossInfo.Platforms[platform] && primeInfo.Platforms[platform]
if hasArch {
return "✓"
}
return "✗"
}

func windowsStatus(expected, exists bool) string {
if !expected {
return "-"
}
if exists {
return "✓"
}
return "✗"
}

func formatImageRef(ref name.Reference) string {
return ref.Context().RepositoryStr() + ":" + ref.Identifier()
}

func table(w io.Writer, results []rke2.Image) {
sort.Slice(results, func(i, j int) bool {
return formatImageRef(results[i].Reference) < formatImageRef(results[j].Reference)
})

missingCount := 0
for _, result := range results {
if !result.OSSImage.Exists || !result.PrimeImage.Exists {
missingCount++
}
}
if missingCount > 0 {
fmt.Fprintln(w, missingCount, "incomplete images")
} else {
fmt.Fprintln(w, "all images OK")
}

tw := tabwriter.NewWriter(w, 0, 8, 2, ' ', 0)
defer tw.Flush()

fmt.Fprintln(tw, "image\toss\tprime\tsig\tamd64\tarm64\twin")
fmt.Fprintln(tw, "-----\t---\t-----\t---\t-----\t-----\t-------")

for _, result := range results {
ossStatus := "✗"
if result.OSSImage.Exists {
ossStatus = "✓"
}
primeStatus := "✗"
if result.PrimeImage.Exists {
primeStatus = "✓"
}
tw.Write([]byte(strings.Join([]string{
formatImageRef(result.Reference),
ossStatus,
primeStatus,
"?", // sigstore not implemented
archStatus(result.ExpectsLinuxAmd64, result.OSSImage, result.PrimeImage, reg.Platform{OS: "linux", Architecture: "amd64"}),
archStatus(result.ExpectsLinuxArm64, result.OSSImage, result.PrimeImage, reg.Platform{OS: "linux", Architecture: "arm64"}),
windowsStatus(result.ExpectsWindows, result.OSSImage.Exists && result.PrimeImage.Exists),
"",
}, "\t") + "\n"))
}
}

func csv(w io.Writer, results []rke2.Image) {
sort.Slice(results, func(i, j int) bool {
return formatImageRef(results[i].Reference) < formatImageRef(results[j].Reference)
})

fmt.Fprintln(w, "image,oss,prime,sig,amd64,arm64,win")

for _, result := range results {
ossStatus := "N"
if result.OSSImage.Exists {
ossStatus = "Y"
}
primeStatus := "N"
if result.PrimeImage.Exists {
primeStatus = "Y"
}

amd64Status := ""
if result.ExpectsLinuxAmd64 {
if result.OSSImage.Platforms[reg.Platform{OS: "linux", Architecture: "amd64"}] &&
result.PrimeImage.Platforms[reg.Platform{OS: "linux", Architecture: "amd64"}] {
amd64Status = "Y"
} else {
amd64Status = "N"
}
}

arm64Status := ""
if result.ExpectsLinuxArm64 {
if result.OSSImage.Platforms[reg.Platform{OS: "linux", Architecture: "arm64"}] &&
result.PrimeImage.Platforms[reg.Platform{OS: "linux", Architecture: "arm64"}] {
arm64Status = "Y"
} else {
arm64Status = "N"
}
}

winStatus := ""
if result.ExpectsWindows {
if result.OSSImage.Exists && result.PrimeImage.Exists {
winStatus = "Y"
} else {
winStatus = "N"
}
}

values := []string{
formatImageRef(result.Reference),
ossStatus,
primeStatus,
"?", // sigstore not implemented
amd64Status,
arm64Status,
winStatus,
}
fmt.Fprintln(w, strings.Join(values, ","))
}
}

var inspectCmd = &cobra.Command{
Use: "inspect [version]",
Short: "Inspect release artifacts",
Long: `Inspect release artifacts for a given version.
Currently supports inspecting the image list for published rke2 releases.`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("expected at least one argument: [version]")
}

ctx := context.Background()
gh := repository.NewGithub(ctx, rootConfig.Auth.GithubToken)
filesystem, err := release.NewFS(ctx, gh, "rancher", "rke2", args[0])
if err != nil {
return err
}

ossClient := reg.NewClient(ossRegistry, debug)

var primeClient *reg.Client
if rootConfig.PrimeRegistry != "" {
primeClient = reg.NewClient(rootConfig.PrimeRegistry, debug)
}

inspector := rke2.NewReleaseInspector(filesystem, ossClient, primeClient, debug)

results, err := inspector.InspectRelease(ctx, args[0])
if err != nil {
return err
}

outputFormat, _ := cmd.Flags().GetString("output")
switch outputFormat {
case "csv":
csv(os.Stdout, results)
default:
table(os.Stdout, results)
}

return nil
},
}

func init() {
rootCmd.AddCommand(inspectCmd)
inspectCmd.Flags().StringP("output", "o", "table", "Output format (table|csv)")
}
106 changes: 106 additions & 0 deletions cmd/release/cmd/inspect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package cmd

import (
"bytes"
"context"
"io/fs"
"os"
"testing"
"testing/fstest"

"github.com/google/go-containerregistry/pkg/name"
reg "github.com/rancher/ecm-distro-tools/registry"
"github.com/rancher/ecm-distro-tools/release/rke2"
)

type mockRegistryClient struct {
images map[string]reg.Image
}

func (m *mockRegistryClient) Image(_ context.Context, ref name.Reference) (reg.Image, error) {
key := ref.Context().RepositoryStr() + ":" + ref.Identifier()
if img, ok := m.images[key]; ok {
return img, nil
}
return reg.Image{Exists: false, Platforms: make(map[reg.Platform]bool)}, nil
}

func newMockFS() fs.FS {
return fstest.MapFS{
"rke2-images-all.linux-amd64.txt": &fstest.MapFile{
Data: []byte("rancher/rke2-runtime:v1.23.4-rke2r1\nrancher/rke2-cloud-provider:v1.23.4-rke2r1"),
},
"rke2-images-all.linux-arm64.txt": &fstest.MapFile{
Data: []byte("rancher/rke2-runtime:v1.23.4-rke2r1"),
},
"rke2-images.windows-amd64.txt": &fstest.MapFile{
Data: []byte("rancher/rke2-runtime-windows:v1.23.4-rke2r1"),
},
}
}

func TestInspectAndCSVOutput(t *testing.T) {
ossImages := map[string]reg.Image{
"rancher/rke2-runtime:v1.23.4-rke2r1": {
Exists: true,
Platforms: map[reg.Platform]bool{
{OS: "linux", Architecture: "amd64"}: true,
{OS: "linux", Architecture: "arm64"}: true,
},
},
"rancher/rke2-cloud-provider:v1.23.4-rke2r1": {
Exists: true,
Platforms: map[reg.Platform]bool{
{OS: "linux", Architecture: "amd64"}: true,
},
},
}

primeImages := map[string]reg.Image{
"rancher/rke2-runtime:v1.23.4-rke2r1": {
Exists: true,
Platforms: map[reg.Platform]bool{
{OS: "linux", Architecture: "amd64"}: true,
{OS: "linux", Architecture: "arm64"}: true,
},
},
"rancher/rke2-cloud-provider:v1.23.4-rke2r1": {
Exists: false,
Platforms: map[reg.Platform]bool{
{OS: "linux", Architecture: "amd64"}: true,
},
},
}

inspector := rke2.NewReleaseInspector(
newMockFS(),
&mockRegistryClient{images: ossImages},
&mockRegistryClient{images: primeImages},
false,
)

results, err := inspector.InspectRelease(context.Background(), "v1.23.4+rke2r1")
if err != nil {
t.Fatalf("InspectRelease() error = %v", err)
}

var buf bytes.Buffer
csv(&buf, results)

expectedBytes, err := os.ReadFile("testdata/inspect_test_output.csv")
if err != nil {
t.Fatalf("failed to read test data: %v", err)
}
expected := string(expectedBytes)
if got := buf.String(); got != expected {
t.Errorf("csv() output = %q, want %q", got, expected)
}
}

func mustParseRef(s string) name.Reference {
ref, err := name.ParseReference(s)
if err != nil {
panic(err)
}
return ref
}
4 changes: 4 additions & 0 deletions cmd/release/cmd/testdata/inspect_test_output.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
image,oss,prime,sig,amd64,arm64,win
rancher/rke2-cloud-provider:v1.23.4-rke2r1,Y,N,?,Y,,
rancher/rke2-runtime-windows:v1.23.4-rke2r1,N,N,?,,,N
rancher/rke2-runtime:v1.23.4-rke2r1,Y,Y,?,Y,Y,
16 changes: 9 additions & 7 deletions cmd/release/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,14 @@ type Auth struct {

// Config
type Config struct {
User *User `json:"user"`
K3s *K3s `json:"k3s" validate:"omitempty"`
Rancher *Rancher `json:"rancher" validate:"omitempty"`
RKE2 *RKE2 `json:"rke2" validate:"omitempty"`
Charts *ChartsRelease `json:"charts" validate:"omitempty"`
Auth *Auth `json:"auth"`
Dashboard *Dashboard `json:"dashboard"`
User *User `json:"user"`
K3s *K3s `json:"k3s" validate:"omitempty"`
Rancher *Rancher `json:"rancher" validate:"omitempty"`
RKE2 *RKE2 `json:"rke2" validate:"omitempty"`
Charts *ChartsRelease `json:"charts" validate:"omitempty"`
Auth *Auth `json:"auth"`
Dashboard *Dashboard `json:"dashboard"`
PrimeRegistry string `json:"prime_registry"`
}

// OpenOnEditor opens the given config file on the user's default text editor.
Expand Down Expand Up @@ -226,6 +227,7 @@ func ExampleConfig() (string, error) {
AWSSessionToken: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
AWSDefaultRegion: "us-east-1",
},
PrimeRegistry: "example.com",
}
b, err := json.MarshalIndent(conf, "", " ")
if err != nil {
Expand Down
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.21
require (
github.com/drone/drone-go v1.7.1
github.com/go-git/go-git/v5 v5.12.1-0.20240807144107-c594bae8d75d
github.com/google/go-containerregistry v0.20.2
github.com/google/go-github/v39 v39.2.0
github.com/sirupsen/logrus v1.9.3
go.opentelemetry.io/otel v1.25.0
Expand Down Expand Up @@ -48,21 +49,31 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.30.5 // indirect
github.com/aws/smithy-go v1.20.4 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/cli v27.1.1+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/fatih/color v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.8 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/vbatts/tar-split v0.11.3 // indirect
go.opentelemetry.io/otel/metric v1.25.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
Expand Down
Loading

0 comments on commit 32c3dab

Please sign in to comment.