package main import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "codeberg.org/woodpecker-plugins/go-plugin" "github.com/rs/zerolog/log" "github.com/urfave/cli/v2" ) type Settings struct { Token string ImageName string Tag string Region string } type Plugin struct { *plugin.Plugin Settings *Settings } func (p *Plugin) Flags() []cli.Flag { return []cli.Flag{ &cli.StringFlag{ Name: "token", Usage: "Token for the Scaleway API", EnvVars: []string{"PLUGIN_TOKEN"}, Destination: &p.Settings.Token, }, &cli.StringFlag{ Name: "image-name", Usage: "The image from which the tag should be deleted", EnvVars: []string{"PLUGIN_IMAGE_NAME"}, Destination: &p.Settings.ImageName, }, &cli.StringFlag{ Name: "tag", Usage: "The tag that should be deleted", EnvVars: []string{"PLUGIN_TAG"}, Destination: &p.Settings.Tag, }, &cli.StringFlag{ Name: "region", Usage: "The Scaleway region the registry exists", EnvVars: []string{"PLUGIN_REGION"}, Destination: &p.Settings.Region, }, } } func (p *Plugin) Execute(ctx context.Context) error { missingFlags := requiredFlags(p.Settings) if len(missingFlags) != 0 { log.Error().Msg("following flags are missing:") for _, errMsg := range missingFlags { log.Error().Msg("-" + errMsg) } return errors.New("missing required flags") } log.Info().Msg("start") token := p.Settings.Token region := p.Settings.Region imageName := p.Settings.ImageName log.Debug().Msg("get image id") imageId, err := getImageId(region, imageName, token) if err != nil { return fmt.Errorf("get image id: %w", err) } tagName := p.Settings.Tag log.Debug().Msg("get tag id") tagId, err := getImageTagId(region, tagName, imageId, token) if err != nil { return fmt.Errorf("get tag id: %w", err) } log.Debug().Msg("delete image id") err = deleteImageTagById(region, tagId, token) if err != nil { return fmt.Errorf("delete tag id: %w", err) } log.Info().Msg("finish") return nil } func requiredFlags(settings *Settings) []string { var errorList []string if settings.ImageName == "" { errorList = append(errorList, "image-name") } if settings.Tag == "" { errorList = append(errorList, "tag") } if settings.Region == "" { errorList = append(errorList, "region") } if settings.Token == "" { errorList = append(errorList, "token") } return errorList } func main() { p := &Plugin{ Settings: &Settings{}, } p.Plugin = plugin.New(plugin.Options{ Name: "Scaleway Delete Image Plugin", Description: "Automatically delete image tags from Scaleway registry", Flags: p.Flags(), Execute: p.Execute, }) p.Run() } type ImagesResponse struct { Images []Image `json:"images"` } type Image struct { Id string `json:"id"` } func getImageId(region, imageName, token string) (string, error) { url := fmt.Sprintf("https://api.scaleway.com/registry/v1/regions/%s/images?name=%s", region, imageName) res, err := doRequest(http.MethodGet, url, token) if err != nil { return "", fmt.Errorf("get request for image id: %w", err) } defer closeLogger(res.Body) if res.StatusCode != 200 { return "", fmt.Errorf("expected status code 200 but got: %d", res.StatusCode) } body, err := io.ReadAll(res.Body) if err != nil { return "", fmt.Errorf("reading response body for image id: %w", err) } var imagesResponse ImagesResponse err = json.Unmarshal(body, &imagesResponse) if err != nil { return "", fmt.Errorf("unmarshal body for image id: %w", err) } if len(imagesResponse.Images) != 1 { return "", fmt.Errorf("expected 1 image but found '%d' with the name: %s", len(imagesResponse.Images), imageName) } return imagesResponse.Images[0].Id, nil } type TagsResponse struct { Tags []Tag `json:"tags"` } type Tag struct { Id string `json:"id"` } func getImageTagId(region, tagName, imageId, token string) (string, error) { url := fmt.Sprintf("https://api.scaleway.com/registry/v1/regions/%s/images/%s/tags?name=%s", region, imageId, tagName) res, err := doRequest(http.MethodGet, url, token) if err != nil { return "", fmt.Errorf("get request tag id: %w", err) } defer closeLogger(res.Body) if res.StatusCode != 200 { return "", fmt.Errorf("expected status code 200 but got: %d", res.StatusCode) } body, err := io.ReadAll(res.Body) if err != nil { return "", fmt.Errorf("reading response body for tag id: %w", err) } var tagsResponse TagsResponse err = json.Unmarshal(body, &tagsResponse) if err != nil { return "", fmt.Errorf("unmarshal body for tag id: %w", err) } if len(tagsResponse.Tags) != 1 { return "", fmt.Errorf("expected 1 but found '%d' with name: %s", len(tagsResponse.Tags), tagName) } return tagsResponse.Tags[0].Id, nil } func deleteImageTagById(region, tagId, token string) error { url := fmt.Sprintf("https://api.scaleway.com/registry/v1/regions/%s/tags/%s", region, tagId) res, err := doRequest(http.MethodDelete, url, token) if err != nil { return fmt.Errorf("deleting tag id: %w", err) } defer closeLogger(res.Body) if res.StatusCode != 200 { return fmt.Errorf("expected status code 200 but got: %d", res.StatusCode) } return nil } func closeLogger(closer io.Closer) { err := closer.Close() if err != nil { log.Warn().Msgf("closing io operation: %s", err.Error()) } } func doRequest(method, url, token string) (*http.Response, error) { req, err := http.NewRequest(method, url, nil) if err != nil { return nil, fmt.Errorf("building request: %w", err) } req.Header.Set("X-Auth-Token", token) res, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("making request: %w", err) } return res, nil }