-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement ORAS helper command #114
Closed
Closed
Changes from 2 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
4203e94
implement azoras cmd
qmuntal a325472
revert go1.21 bump in go.mod
qmuntal 3f6fcc4
Apply suggestions from code review
qmuntal 99c1d7b
improve oras auth section
qmuntal 017f4d9
add azoras version subcommand
qmuntal 5739847
embed unmodified oras checksums file
qmuntal cd68b86
error out if UntarOneFile detects a duplicated file
qmuntal 9d6a8e6
install oras in custom location
qmuntal c8105ba
append bulk images to positional arg images
qmuntal 48cba95
fix bulk argument parsing
qmuntal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# azoras | ||
|
||
azoras is a tool that helps work with the ORAS CLI and Azure ACR. | ||
The subcommands implement common workflows for image annotations and maintenance. | ||
|
||
## Installation | ||
|
||
```shell | ||
go install github.com/go-infra/cmd/azoras@latest | ||
``` | ||
|
||
Optionally, azoras can install the ORAS CLI for you running: | ||
|
||
```shell | ||
azoras install | ||
``` | ||
|
||
## Authentication | ||
|
||
azoras doesn't handle authentication, you need to login to your Azure ACR registry with the ORAS CLI. | ||
See https://oras.land/docs/how_to_guides/authentication for more information. | ||
|
||
azoras provides a helper subcommand to login to login the ORAS CLI to your Azure ACR registry using the Azure CLI. | ||
qmuntal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
You first need to login to the Azure CLI with `az login` and then run: | ||
|
||
```shell | ||
azoras login | ||
``` | ||
qmuntal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## Subcommands | ||
|
||
### `azoras deprecate` | ||
|
||
Deprecate an image in an Azure ACR registry. | ||
|
||
```shell | ||
azoras deprecate myregistry.azurecr.io/myimage:sha256:foo | ||
``` | ||
|
||
This subcommand can also deprecated multiple images at once using a file with a line-separated list of images. | ||
|
||
```shell | ||
azoras deprecate images.txt -bulk | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
package main | ||
qmuntal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
import ( | ||
"log" | ||
|
||
"github.com/microsoft/go-infra/subcmd" | ||
) | ||
|
||
// version is the semver of this tool. Compared against the value in the config file (if any) to | ||
// ensure that all users of the tool contributing to a given repo have a new enough version of the | ||
// tool to support all patching features used in that repo. | ||
// | ||
// When adding a new feature to the azoras tool, make sure it is backward compatible and | ||
// increment the patch number here. | ||
const version = "v1.0.0" | ||
qmuntal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const description = ` | ||
azoras is a tool that helps work with the ORAS CLI and Azure ACR. | ||
The subcommands implement common workflows for image annotations and maintenance. | ||
` | ||
|
||
var subcommands []subcmd.Option | ||
|
||
func main() { | ||
if err := subcmd.Run("azoras", description, subcommands); err != nil { | ||
log.Fatal(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"flag" | ||
"log" | ||
"os" | ||
"os/exec" | ||
"strings" | ||
"time" | ||
|
||
"github.com/microsoft/go-infra/executil" | ||
"github.com/microsoft/go-infra/subcmd" | ||
ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
) | ||
|
||
const ( | ||
artifactTypeLifecycle = "application/vnd.microsoft.artifact.lifecycle" | ||
annotationNameEoL = "vnd.microsoft.artifact.lifecycle.end-of-life.date" | ||
) | ||
|
||
func init() { | ||
subcommands = append(subcommands, subcmd.Option{ | ||
Name: "deprecate", | ||
Summary: "Deprecate the given image by annotating it with an end-of-life date.", | ||
Description: ` | ||
Examples: | ||
|
||
go run ./cmd/azoras deprecate myregistry.azurecr.io/myimage:sha256:foo | ||
go run ./cmd/azoras deprecate images.txt -bulk -eol 2022-12-31T23:59:59Z | ||
`, | ||
Handle: handleDeprecate, | ||
TakeArgsReason: "The fully qualified image to deprecate or a file containing a newline-separated list of images to deprecate if -bulk is set.", | ||
}) | ||
} | ||
|
||
func handleDeprecate(p subcmd.ParseFunc) error { | ||
eolStr := flag.String("eol", "", "The end-of-life date for the image in RFC3339 format. Defaults to the current time.") | ||
bulk := flag.Bool("bulk", false, "Deprecate multiple images.") | ||
qmuntal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err := p(); err != nil { | ||
return err | ||
} | ||
if _, err := exec.LookPath("oras"); err != nil { | ||
return err | ||
} | ||
ref := flag.Arg(0) | ||
eol := time.Now() | ||
if *eolStr != "" { | ||
var err error | ||
eol, err = time.Parse(time.RFC3339, *eolStr) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
if !*bulk { | ||
// Deprecate a single image. | ||
return deprecate(ref, eol) | ||
} | ||
|
||
// Deprecate multiple images. | ||
data, err := os.ReadFile(ref) | ||
if err != nil { | ||
return err | ||
} | ||
images := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") | ||
return deprecateBulk(images, eol) | ||
} | ||
|
||
// deprecateBulk deprecates multiple images in bulk. | ||
func deprecateBulk(images []string, eol time.Time) error { | ||
var err error | ||
for _, image := range images { | ||
if err1 := deprecate(image, eol); err1 != nil { | ||
log.Printf("Failed to deprecate image %s: %v\n", image, err1) | ||
if err == nil { | ||
err = err1 | ||
} | ||
} | ||
} | ||
return err | ||
} | ||
|
||
// deprecate annotates the given image with an end-of-life date. | ||
func deprecate(image string, eol time.Time) error { | ||
prevs, err := getAnnotation(image, artifactTypeLifecycle, annotationNameEoL) | ||
if err != nil { | ||
// Log the error and continue with the deprecation, as this is a best-effort operation. | ||
log.Printf("Failed to get the EoL date for image %s: %v\n", image, err) | ||
} | ||
for _, prev := range prevs { | ||
t, err := time.Parse(time.RFC3339, prev) | ||
if err == nil && t.Before(eol) { | ||
// The image is already deprecated. | ||
log.Printf("Image %s is already past its EoL date of %s\n", image, prev) | ||
return nil | ||
} | ||
} | ||
cmdOras := exec.Command("oras", "attach", "--artifact-type", artifactTypeLifecycle, "--annotation", annotationNameEoL+"="+eol.Format(time.RFC3339), image) | ||
qmuntal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
err = executil.Run(cmdOras) | ||
if err != nil { | ||
return err | ||
} | ||
log.Printf("Image %s deprecated with an EoL date of %s\n", image, eol.Format(time.RFC3339)) | ||
return nil | ||
} | ||
|
||
// getAnnotation returns the list of values for the given annotation name on the given image. | ||
func getAnnotation(image, artifactType, name string) ([]string, error) { | ||
cmd := exec.Command("oras", "discover", "-o", "json", image) | ||
out, err := executil.CombinedOutput(cmd) | ||
if err != nil { | ||
return nil, err | ||
} | ||
var index ocispec.Index | ||
if err := json.Unmarshal([]byte(out), &index); err != nil { | ||
return nil, err | ||
} | ||
var vals []string | ||
for _, manifest := range index.Manifests { | ||
if manifest.ArtifactType != artifactType { | ||
continue | ||
} | ||
for key, val := range manifest.Annotations { | ||
if key == name { | ||
vals = append(vals, val) | ||
} | ||
} | ||
} | ||
return vals, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
package main | ||
|
||
import ( | ||
"bytes" | ||
"crypto/sha256" | ||
"fmt" | ||
"io" | ||
"log" | ||
"net/http" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"runtime" | ||
|
||
"github.com/microsoft/go-infra/internal/archive" | ||
"github.com/microsoft/go-infra/subcmd" | ||
) | ||
|
||
const ( | ||
orasVersion = "1.1.0" | ||
) | ||
|
||
// checksums is copied from https://github.com/oras-project/oras/releases/download/v1.1.0/oras_1.1.0_checksums.txt. | ||
var checksums = map[string]string{ | ||
"oras_1.1.0_linux_s390x.tar.gz": "067600d61d5d7c23f7bd184cff168ad558d48bed99f6735615bce0e1068b1d77", | ||
"oras_1.1.0_windows_amd64.zip": "2ac83631181d888445e50784a5f760f7f9d97fba3c089e79b68580c496fe68cf", | ||
"oras_1.1.0_darwin_arm64.tar.gz": "d52d3140b0bb9f7d7e31dcbf2a513f971413769c11f7d7a5599e76cc98e45007", | ||
"oras_1.1.0_linux_armv7.tar.gz": "def86e7f787f8deee50bb57d1c155201099f36aa0c6700d3b525e69ddf8ae49b", | ||
"oras_1.1.0_linux_amd64.tar.gz": "e09e85323b24ccc8209a1506f142e3d481e6e809018537c6b3db979c891e6ad7", | ||
"oras_1.1.0_linux_arm64.tar.gz": "e450b081f67f6fda2f16b7046075c67c9a53f3fda92fd20ecc59873b10477ab4", | ||
"oras_1.1.0_darwin_amd64.tar.gz": "f8ac5dea53dd9331cf080f1025f0612e7b07c5af864a4fd609f97d8946508e45", | ||
qmuntal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
func init() { | ||
subcommands = append(subcommands, subcmd.Option{ | ||
Name: "install", | ||
Summary: "Install the ORAS CLI.", | ||
Description: "", | ||
Handle: handleInit, | ||
}) | ||
} | ||
|
||
func handleInit(p subcmd.ParseFunc) error { | ||
if err := p(); err != nil { | ||
return err | ||
} | ||
if path, err := exec.LookPath("oras"); err == nil { | ||
log.Printf("ORAS CLI is already installed at %s\n", path) | ||
return nil | ||
} | ||
dir, err := installDir() | ||
if err != nil { | ||
return err | ||
} | ||
if err := downloadAndInstallOras(dir); err != nil { | ||
os.RemoveAll(dir) // Best effort to clean up the directory. | ||
qmuntal marked this conversation as resolved.
Show resolved
Hide resolved
qmuntal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return err | ||
} | ||
return nil | ||
} | ||
|
||
// downloadOras downloads the ORAS CLI and installs it in the recommended location. | ||
// See https://oras.land/docs/installation/ for more information. | ||
func downloadAndInstallOras(dir string) error { | ||
targetFile := orasGitHubFileName() | ||
content, err := download(targetFile) | ||
if err != nil { | ||
return err | ||
} | ||
return install(dir, content, filepath.Ext(targetFile)) | ||
} | ||
|
||
// install installs the ORAS CLI in the given directory. | ||
func install(dir string, content []byte, format string) error { | ||
log.Println("Extacting ORAS CLI...") | ||
name := orasBinName() | ||
content, err := extract(format, name, content) | ||
if err != nil { | ||
return err | ||
} | ||
log.Println("Extracted ORAS CLI") | ||
|
||
bin := filepath.Join(dir, name) | ||
log.Printf("Installing ORAS CLI to %#q...\n", bin) | ||
if err := os.WriteFile(bin, content, 0o755); err != nil { | ||
return err | ||
} | ||
log.Println("Installed ORAS CLI") | ||
if runtime.GOOS == "windows" { | ||
log.Printf("Add %#q to your PATH environment variable so that oras.exe can be found.\n", dir) | ||
} | ||
return nil | ||
} | ||
|
||
// download fetches the ORAS CLI from the GitHub release page. | ||
func download(name string) ([]byte, error) { | ||
expectedChecksum, ok := checksums[name] | ||
if !ok { | ||
return nil, fmt.Errorf("unable to find checksum for %s", name) | ||
} | ||
target := fmt.Sprintf("https://github.com/oras-project/oras/releases/download/v%s/%s", orasVersion, name) | ||
log.Printf("Downloading ORAS CLI from %s\n", target) | ||
resp, err := http.Get(target) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to download ORAS CLI: %v", err) | ||
} | ||
defer resp.Body.Close() | ||
if resp.StatusCode != http.StatusOK { | ||
return nil, fmt.Errorf("unable to download ORAS CLI: %s", resp.Status) | ||
} | ||
content, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to read response body: %v", err) | ||
} | ||
|
||
// Validate the checksum | ||
sum := fmt.Sprintf("%x", sha256.Sum256(content)) | ||
if sum != expectedChecksum { | ||
return nil, fmt.Errorf("SHA256 mismatch.\n Expected: %v\n Downloaded: %v", expectedChecksum, sum) | ||
} | ||
log.Println("Downloaded ORAS CLI") | ||
return content, nil | ||
} | ||
|
||
// extract extracts the ORAS CLI from the archive. | ||
func extract(format string, name string, data []byte) (content []byte, err error) { | ||
switch format { | ||
case ".zip": | ||
content, err = archive.UnzipOneFile(name, bytes.NewReader(data), int64(len(data))) | ||
case ".tar.gz": | ||
content, err = archive.UntarOneFile(name, bytes.NewReader(data), true) | ||
default: | ||
return nil, fmt.Errorf("unsupported archive format: %s", format) | ||
} | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to extract ORAS CLI: %v", err) | ||
} | ||
if content == nil { | ||
return nil, fmt.Errorf("unable to find %s in the archive", name) | ||
} | ||
return content, nil | ||
} | ||
|
||
func orasGitHubFileName() string { | ||
arch := runtime.GOARCH | ||
if arch == "arm" { | ||
// ORAS uses "armv7" instead of "arm" in the URL. | ||
arch = "armv7" | ||
} | ||
ext := "tar.gz" | ||
if runtime.GOOS == "windows" { | ||
ext = "zip" | ||
} | ||
return fmt.Sprintf("oras_%s_%s_%s.%s", orasVersion, runtime.GOOS, arch, ext) | ||
} | ||
|
||
func installDir() (string, error) { | ||
var dir string | ||
switch runtime.GOOS { | ||
case "windows": | ||
dir = filepath.Join(os.Getenv("USERPROFILE"), "bin") | ||
default: | ||
dir = "/usr/local/bin" | ||
qmuntal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
err := os.MkdirAll(dir, 0o755) | ||
return dir, err | ||
} | ||
|
||
func orasBinName() string { | ||
ext := "" | ||
if runtime.GOOS == "windows" { | ||
ext = ".exe" | ||
} | ||
return "oras" + ext | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
azoras doesn't handle authentication
seems to conflict withazoras provides a helper subcommand to login
. I think I get what this is trying to say (it won't use any ambient auth if you go directly to run theazoras deprecate
command) but that doesn't seem all that important vs. listing the relatively painless commands that you actually need to run to get it set up.I think this could be moved into a note after the instructions, and I don't think it needs to be worded like
azoras
is lacking a major feature, just pointing out what's going on behind the scenes.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I'll rephrase this section.