Skip to content

Commit

Permalink
check images promotion pipelines in hashrelease promotions
Browse files Browse the repository at this point in the history
  • Loading branch information
radTuti committed Feb 10, 2025
1 parent 690a477 commit ac0c741
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 1 deletion.
7 changes: 6 additions & 1 deletion release/cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ var (
// External flags are flags used to interact with external services
var (
// CI flags for interacting with CI services (Semaphore)
ciFlags = []cli.Flag{ciFlag, ciBaseURLFlag, ciJobIDFlag}
ciFlags = []cli.Flag{ciFlag, ciBaseURLFlag, ciJobIDFlag, ciTokenFlag}
semaphoreCI = "semaphore"
ciFlag = &cli.BoolFlag{
Name: "ci",
Expand All @@ -282,6 +282,11 @@ var (
Usage: fmt.Sprintf("The job ID for the %s CI job", semaphoreCI),
EnvVars: []string{"SEMAPHORE_JOB_ID"},
}
ciTokenFlag = &cli.StringFlag{
Name: "ci-token",
Usage: fmt.Sprintf("The token for interacting with %s API", semaphoreCI),
EnvVars: []string{"SEMAPHORE_API_TOKEN"},
}

// Slack flags for posting messages to Slack
slackFlags = []cli.Flag{slackTokenFlag, slackChannelFlag, notifyFlag}
Expand Down
39 changes: 39 additions & 0 deletions release/cmd/hashrelease.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ package main

import (
"fmt"
"os"
"path/filepath"
"strconv"

"github.com/sirupsen/logrus"
cli "github.com/urfave/cli/v2"

"github.com/projectcalico/calico/release/internal/ci"
"github.com/projectcalico/calico/release/internal/hashreleaseserver"
"github.com/projectcalico/calico/release/internal/imagescanner"
"github.com/projectcalico/calico/release/internal/outputs"
Expand Down Expand Up @@ -64,6 +67,10 @@ func hashreleaseSubCommands(cfg *Config) []*cli.Command {
return err
}

if err := validateCIBuildRequirements(c, cfg.RepoRootDir); err != nil {
return err
}

// Clone the operator repository.
operatorDir := filepath.Join(cfg.TmpDir, operator.DefaultRepoName)
err := operator.Clone(c.String(operatorOrgFlag.Name), c.String(operatorRepoFlag.Name), c.String(operatorBranchFlag.Name), operatorDir)
Expand Down Expand Up @@ -306,6 +313,9 @@ func validateHashreleaseBuildFlags(c *cli.Context) error {
return fmt.Errorf("missing hashrelease server configuration, must set %s, %s, %s, %s, and %s",
sshHostFlag, sshUserFlag, sshKeyFlag, sshPortFlag, sshKnownHostsFlag)
}
if c.String(ciTokenFlag.Name) == "" {
return fmt.Errorf("%s API token must be set when running on CI, either set \"SEMAPHORE_API_TOKEN\" or use %s flag", semaphoreCI, ciTokenFlag.Name)
}
} else {
// If building images, log a warning if no registry is specified.
if c.Bool(buildHashreleaseImageFlag.Name) && len(c.StringSlice(registryFlag.Name)) == 0 {
Expand Down Expand Up @@ -381,3 +391,32 @@ func imageScanningAPIConfig(c *cli.Context) *imagescanner.Config {
Scanner: c.String(imageScannerSelectFlag.Name),
}
}

func validateCIBuildRequirements(c *cli.Context, repoRootDir string) error {
if !c.Bool(ciFlag.Name) {
return nil
}
if c.Bool(buildImagesFlag.Name) {
logrus.Debug("Building images, skipping images promotions check...")
return nil
}
orgURL := c.String(ciBaseURLFlag.Name)
token := c.String(ciTokenFlag.Name)
pipelineID := c.String(ciJobIDFlag.Name)
if promotion, err := strconv.ParseBool(os.Getenv("SEMAPHORE_PIPELINE_PROMOTION")); err != nil {
return fmt.Errorf("failed to parse promotion environment variable: %v", err)
} else if promotion {
logrus.Info("This is a promotion pipeline, checking if all images promotion pipelines have passed...")
pipelineID = os.Getenv("SEMAPHORE_PIPELINE_0_ARTEFACT_ID")
} else {
logrus.Info("This is a regular pipeline, skipping images promotions check...")
}
promotionsDone, err := ci.ImagePromotionsDone(repoRootDir, orgURL, pipelineID, token)
if err != nil {
return fmt.Errorf("failed to check if images promotions are done: %v", err)
}
if !promotionsDone {
return fmt.Errorf("images promotions are not done, wait for all images promotions to pass before publishing the hashrelease")
}
return nil
}
128 changes: 128 additions & 0 deletions release/internal/ci/semaphore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package ci

import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"

"github.com/sirupsen/logrus"

"github.com/projectcalico/calico/release/internal/command"
)

const passed = "passed"

type promotion struct {
Status string `json:"status"`
Name string `json:"name"`
PipelineID string `json:"scheduled_pipeline_id"`
}

type pipeline struct {
Result string `json:"result"`
}

func apiURL(orgURL, path string) string {
orgURL = strings.TrimPrefix(orgURL, "/")
path = strings.TrimSuffix(path, "/")
return fmt.Sprintf("%s/api/v1alpha/%s", orgURL, path)
}

func fetchPromotions(orgURL, pipelineID, token string) ([]promotion, error) {
url := apiURL(orgURL, "/promotions")
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create %s request: %s", url, err.Error())
}
req.Header.Set("Authorization", fmt.Sprintf("Token %s", token))
q := req.URL.Query()
q.Add("pipeline_id", pipelineID)
req.URL.RawQuery = q.Encode()

logrus.WithField("url", req.URL.String()).Debug("get pipeline promotions")

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to request promotions: %s", err.Error())
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch promotions")
}

var promotions []promotion
if err := json.NewDecoder(resp.Body).Decode(&promotions); err != nil {
return nil, fmt.Errorf("failed to parse promotions: %s", err.Error())
}

var imagesPromotions []promotion
for _, p := range promotions {
if strings.HasPrefix(strings.ToLower(p.Name), "push ") {
imagesPromotions = append(imagesPromotions, p)
}
}
return imagesPromotions, nil
}

func getPipelineResult(orgURL, pipelineID, token string) (*pipeline, error) {
url := apiURL(orgURL, fmt.Sprintf("/pipeline/%s", pipelineID))
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create %s request: %s", url, err.Error())
}
req.Header.Set("Authorization", fmt.Sprintf("Token %s", token))

logrus.WithField("url", req.URL.String()).Debug("get pipeline details")

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to request pipeline details: %s", err.Error())
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch pipeline details")
}

var p pipeline
if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
return nil, fmt.Errorf("failed to parse pipeline: %s", err.Error())
}

return &p, err
}

func ImagePromotionsDone(repoRootDir, orgURL, pipelineID, token string) (bool, error) {
expectPromotionCountStr, err := command.Run("grep", []string{"-c", `"name: Push "`, fmt.Sprintf("%s/.semaphore/semaphore.yml.d/03-promotions.yml")})
if err != nil {
return false, fmt.Errorf("failed to get expected image promotions")
}
expectedPromotionCount, err := strconv.Atoi(expectPromotionCountStr)
if err != nil {
return false, fmt.Errorf("unable to convert expected promotions to int")
}
promotions, err := fetchPromotions(orgURL, pipelineID, token)
if err != nil {
return false, err
}
promotionsCount := len(promotions)
if promotionsCount < expectedPromotionCount {
return false, fmt.Errorf("number of promotions do not match: expected %d, got %d", expectedPromotionCount, promotionsCount)
}
for _, promotion := range promotions {
if promotion.Status != passed {
logrus.WithField("promotion", promotion.Name).Error("triggering promotion failed")
return false, fmt.Errorf("triggering %q promotion failed, cannot check pipeline result", promotion.Name)
}
pipeline, err := getPipelineResult(orgURL, promotion.PipelineID, token)
if err != nil {
return false, fmt.Errorf("unable to get %q pipeline details", promotion.Name)
}
if pipeline.Result != passed {
logrus.WithField("promotion", promotion.Name).Error("promotion failed")
return false, fmt.Errorf("%q promotion failed", promotion.Name)
}
}
return true, nil
}

0 comments on commit ac0c741

Please sign in to comment.