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

Introduce IBM Container Registry delete #1043

Merged
merged 1 commit into from
Apr 21, 2022
Merged
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
176 changes: 169 additions & 7 deletions cmd/bundle/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ import (
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"

"github.com/docker/cli/cli/config"
"github.com/golang-jwt/jwt/v4"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/empty"
Expand Down Expand Up @@ -210,12 +213,20 @@ func ResolveAuthBasedOnTargetUsingConfigFile(ref name.Reference, dockerConfigFil
// with an empty image (to remove the content, and save quota).
// - Edge case would be no tags in the repository, which is ignored.
//
// IBM Container Registry images:
// Custom delete API call has to be used, since ICR does not support the
// default registry API for deletions. The credentials need to have an
// IBM API key, which is used to obtain an identity token that needs to
// contains the respective authorization token for requests as well as
// an account identifier to select the IBM account in which the registry
// namespace and image is located.
//
// Other registries:
// Use standard spec delete API request to delete the provided tag.
//
func Prune(ctx context.Context, ref name.Reference, auth authn.Authenticator) error {
switch ref.Context().RegistryStr() {
case "index.docker.io":
switch {
case strings.Contains(ref.Context().RegistryStr(), "docker.io"):
list, err := remote.List(ref.Context(), remote.WithContext(ctx), remote.WithAuth(auth))
if err != nil {
return err
Expand Down Expand Up @@ -262,6 +273,24 @@ func Prune(ctx context.Context, ref name.Reference, auth authn.Authenticator) er
)
}

case strings.Contains(ref.Context().RegistryStr(), "icr.io"):
authr, err := auth.Authorization()
if err != nil {
return err
}

// IBM Container Registry API calls will only work in case an API key is available
if authr.Username != "iamapikey" {
return fmt.Errorf("unable to delete image %q, the provided access credentials do not contain an IBM API key", ref.String())
}

token, accountID, err := icrLogin(authr.Password)
if err != nil {
return err
}

return icrDelete(token, accountID, ref)

default:
return remote.Delete(
ref,
Expand All @@ -271,6 +300,12 @@ func Prune(ctx context.Context, ref name.Reference, auth authn.Authenticator) er
}
}

func httpClient() *http.Client {
return &http.Client{
Timeout: 30 * time.Second,
}
}

func dockerHubLogin(username string, password string) (string, error) {
type LoginData struct {
Username string `json:"username"`
Expand All @@ -290,7 +325,7 @@ func dockerHubLogin(username string, password string) (string, error) {
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")

resp, err := http.DefaultClient.Do(req)
resp, err := httpClient().Do(req)
if err != nil {
return "", err
}
Expand All @@ -313,7 +348,7 @@ func dockerHubLogin(username string, password string) (string, error) {
return "", err
}

return loginToken.Token, nil
return fmt.Sprintf("JWT %s", loginToken.Token), nil

default:
return "", fmt.Errorf(string(bodyData))
Expand All @@ -326,9 +361,9 @@ func dockerHubRepoDelete(token string, ref name.Reference) error {
return err
}

req.Header.Set("Authorization", "JWT "+token)
req.Header.Set("Authorization", token)

resp, err := http.DefaultClient.Do(req)
resp, err := httpClient().Do(req)
if err != nil {
return err
}
Expand All @@ -345,6 +380,133 @@ func dockerHubRepoDelete(token string, ref name.Reference) error {
return nil

default:
return fmt.Errorf("failed with HTTP status code %d: %s", resp.StatusCode, string(respData))
return fmt.Errorf("failed to delete image %q: %s (HTTP status code %d)",
ref.String(),
string(respData),
resp.StatusCode,
)
}
}

func icrLogin(apikey string) (string, string, error) {
data := fmt.Sprintf("grant_type=%s&apikey=%s",
url.QueryEscape("urn:ibm:params:oauth:grant-type:apikey"),
apikey,
)

req, err := http.NewRequest("POST", "https://iam.cloud.ibm.com/identity/token", strings.NewReader(data))
SaschaSchwarze0 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return "", "", err
}

req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")

resp, err := httpClient().Do(req)
if err != nil {
return "", "", err
}

defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", "", err
}

switch resp.StatusCode {
case http.StatusOK:
type ibmCloudIdentityToken struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
ExpiresIn int64 `json:"expires_in"`
Expiration int64 `json:"expiration"`
}

var identityToken ibmCloudIdentityToken
if err := json.Unmarshal(body, &identityToken); err != nil {
return "", "", err
}

var token = fmt.Sprintf("%s %s", identityToken.TokenType, identityToken.AccessToken)

var accountID string
_, _ = jwt.Parse(identityToken.AccessToken, func(t *jwt.Token) (interface{}, error) {
switch obj := t.Claims.(type) {
case jwt.MapClaims:
if account, ok := obj["account"]; ok {
switch accountMap := account.(type) {
case map[string]interface{}:
switch tmp := accountMap["bss"].(type) {
case string:
accountID = tmp
}
}
}
}

return nil, nil
})

if accountID == "" {
return "", "", fmt.Errorf("failed to obtain account ID from identity token")
}

return token, accountID, nil

default:
var responseMsg map[string]interface{}
if err := json.Unmarshal(body, &responseMsg); err != nil {
return "", "", err
}

errorCode, errorCodeFound := responseMsg["errorCode"]
errorMessage, errorMessageFound := responseMsg["errorMessage"]
if errorCodeFound && errorMessageFound {
return "", "", fmt.Errorf("failed to obtain identity token from IAM: %v (%v)", errorMessage, errorCode)
}

return "", "", fmt.Errorf("failed to obtain identity token from IAM: %s", string(body))
}
}

func icrDelete(token string, accountID string, ref name.Reference) error {
deleteURL := fmt.Sprintf("https://%s/api/v1/images/%s",
ref.Context().RegistryStr(),
url.QueryEscape(ref.String()),
)

req, err := http.NewRequest("DELETE", deleteURL, nil)
if err != nil {
return err
}

req.Header.Set("Account", accountID)
req.Header.Set("Authorization", token)
req.Header.Set("Accept", "application/json")

resp, err := httpClient().Do(req)
if err != nil {
return err
}

defer resp.Body.Close()

respData, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}

switch resp.StatusCode {
case http.StatusOK:
return nil

default:
return fmt.Errorf("failed to delete image %q: %s (HTTP status code %d)",
ref.String(),
string(respData),
resp.StatusCode,
)
}
}
1 change: 1 addition & 0 deletions cmd/bundle/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ var _ = Describe("Bundle Loader", func() {
withTempDir(func(target string) {
Expect(run(
"--image", testImage,
"--secret-path", dockerConfigFile,
"--target", target,
)).To(Succeed())

Expand Down