Skip to content
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
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions cmd/azoras/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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 provides a helper subcommand to login the ORAS CLI to your Azure ACR registry using the Azure CLI.

1. Log into the Azure CLI:
```shell
az login
```
2. Use the utility command to log into your ACR:
```shell
azoras login <acr-name>
```
An ACR name is `golangimages`, *not* its login server URL.

It is also possible to login to the ORAS CLI manually using the `oras login` command.
See the [ORAS CLI Authentication](https://oras.land/docs/how_to_guides/authentication/) documentation for more information.

## 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 -bulk images.txt
```
31 changes: 31 additions & 0 deletions cmd/azoras/azoras.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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)
}
}
7 changes: 7 additions & 0 deletions cmd/azoras/checksums/oras_1.1.0_checksums.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
067600d61d5d7c23f7bd184cff168ad558d48bed99f6735615bce0e1068b1d77 oras_1.1.0_linux_s390x.tar.gz
2ac83631181d888445e50784a5f760f7f9d97fba3c089e79b68580c496fe68cf oras_1.1.0_windows_amd64.zip
d52d3140b0bb9f7d7e31dcbf2a513f971413769c11f7d7a5599e76cc98e45007 oras_1.1.0_darwin_arm64.tar.gz
def86e7f787f8deee50bb57d1c155201099f36aa0c6700d3b525e69ddf8ae49b oras_1.1.0_linux_armv7.tar.gz
e09e85323b24ccc8209a1506f142e3d481e6e809018537c6b3db979c891e6ad7 oras_1.1.0_linux_amd64.tar.gz
e450b081f67f6fda2f16b7046075c67c9a53f3fda92fd20ecc59873b10477ab4 oras_1.1.0_linux_arm64.tar.gz
f8ac5dea53dd9331cf080f1025f0612e7b07c5af864a4fd609f97d8946508e45 oras_1.1.0_darwin_amd64.tar.gz
135 changes: 135 additions & 0 deletions cmd/azoras/deprecate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package main

import (
"bytes"
"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 -bulk images.txt -eol 2022-12-31T23:59:59Z
`,
Handle: handleDeprecate,
TakeArgsReason: "A list of fully-qualified image to deprecate",
})
}

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.String("bulk", "", "A file containing a line-separated list of images to deprecate in bulk.")
if err := p(); err != nil {
return err
}
if _, err := exec.LookPath("oras"); err != nil {
return err
}

eol := time.Now()
if *eolStr != "" {
var err error
eol, err = time.Parse(time.RFC3339, *eolStr)
if err != nil {
return err
}
}
images := append([]string{}, flag.Args()...)
if *bulk != "" {
data, err := os.ReadFile(*bulk)
if err != nil {
return err
}
data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))
images = append(images, strings.Split(string(data), "\n")...)
}

var err error
for _, image := range images {
if image == "" {
continue
}
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)
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
}
Loading