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

add semaphore pipeline validator #594

Merged
merged 2 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 17 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ FROM calico/bpftool:v7.4.0 as bpftool

FROM --platform=amd64 calico/qemu-user-static:latest as qemu

FROM --platform=amd64 golang:latest AS sembuilder

ARG TARGETARCH

WORKDIR /semhome

COPY semvalidator/go.mod semvalidator/go.sum ./

RUN go mod download

COPY semvalidator/main.go .

RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -o semvalidator .

hjiawei marked this conversation as resolved.
Show resolved Hide resolved
FROM registry.access.redhat.com/ubi8/ubi:latest as ubi

ARG TARGETARCH
Expand Down Expand Up @@ -195,6 +209,9 @@ COPY ssh_known_hosts /etc/ssh/ssh_known_hosts
# Add bpftool for Felix UT/FV.
COPY --from=bpftool /bpftool /usr/bin

# Add semvalidator
COPY --from=sembuilder /semhome/semvalidator /usr/bin/

COPY entrypoint.sh /usr/local/bin/entrypoint.sh

# Squash into a single layer
Expand Down
51 changes: 51 additions & 0 deletions semvalidator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# semvalidator

This allows running validations on semaphore pipeline files.

## Usage

The help give all the required options.

```sh
$ docker run --rm calico/go-build:${GOBUILD_VERSION} semvalidator --help
Usage of semvalidator:
-debug
enable debug logging
-dirs string
comma separated list of directories to search for Semaphore pipeline files
-files string
comma separated list of Semaphore pipeline files
-org string
Semaphore organization
-skip-dirs string
comma separated list of directories to skip when searching for Semaphore pipeline files
-token string
Semaphore API token
```

You can specify dirs that contain semaphore pipeline files (using `-dirs`)
and/or files that are semphore pipeline files (using `-files`).

The organization is need to determine the Semaphore instance.
It will also use the value of `SEMAPHORE_ORGANIZATION` environment variable if flag is empty.

The token needs to be a valid [Semaphore API token](https://docs.semaphoreci.com/reference/api-v1alpha/#authentication).
It will try to use the `SEMAPHORE_API_TOKEN` environment variable if flag is empty.

### Examples

Using `latest` as `${GOBUILD_VERSION}`

1. Give a project `<path-to-dir>` with semaphore files in `<path-to-dir>/.semaphore` directory,
below is how to validate the files in that directory.

```sh
docker run --rm -v <path-to-dir>:<location-in-container>:ro calico/go-build:latest semvalidator -dirs <location-in-container>/.semaphore -org <semaphore-organization> -token <semaphore-token>
```

1. Give a project `<path-to-dir>` with semaphore file in `<path-to-dir>/.semaphore/semaphore.yml` directory,
below is how to validate the files in that directory.

```sh
docker run --rm -v <path-to-dir>:<location-in-container>:ro calico/go-build:latest semvalidator -files <path-to-dir>/.semaphore/semaphore.yml -org <semaphore-organization> -token <semaphore-token>
```
7 changes: 7 additions & 0 deletions semvalidator/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/projectcalico/semvalidator

go 1.23.2

require github.com/sirupsen/logrus v1.9.3

require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
15 changes: 15 additions & 0 deletions semvalidator/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
164 changes: 164 additions & 0 deletions semvalidator/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright (c) 2024 Tigera, Inc. All rights reserved.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"bytes"
"encoding/json"
"flag"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"

"github.com/sirupsen/logrus"
)

var (
dir string
skipDir string
file string
org string
token string
debug bool
)

func init() {
flag.StringVar(&dir, "dirs", "", "comma separated list of directories to search for Semaphore pipeline files")
flag.StringVar(&skipDir, "skip-dirs", "", "comma separated list of directories to skip when searching for Semaphore pipeline files")
flag.StringVar(&file, "files", "", "comma separated list of Semaphore pipeline files")
flag.StringVar(&org, "org", os.Getenv("SEMAPHORE_ORGANIZATION"), "Semaphore organization")
flag.StringVar(&token, "token", "", "Semaphore API token")
flag.BoolVar(&debug, "debug", false, "enable debug logging")
}

func inSkipDirs(path string, skipDirs []string) bool {
for _, skipDir := range skipDirs {
if strings.HasSuffix(path, skipDir) {
return true
}
}
return false
}

func getPipelineYAMLFiles(dir string, skipDirs []string) ([]string, error) {
var files []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip the YAML .semaphore/semaphore.yml.d directory
// as it contains building blocks which are not full pipeline definitions
// The resulting pipeline will be validated as part of semaphore.yml and semaphore-scheduled-builds.yml
if info.IsDir() && !inSkipDirs(path, skipDirs) {
return filepath.SkipDir
}
if !info.IsDir() && (filepath.Ext(path) == ".yml" || filepath.Ext(path) == ".yaml") {
files = append(files, path)
}
return nil
})
return files, err
}

func validateYAML(file, org, token string) error {
logrus.WithField("file", file).Info("validating YAML")
content, err := os.ReadFile(file)
if err != nil {
logrus.WithError(err).Error("failed to read file")
return err
}
payload := map[string]string{
"yaml_definition": fmt.Sprintf("%v", string(content)),
}
data, err := json.Marshal(payload)
if err != nil {
logrus.WithError(err).Error("failed to marshal payload for yaml validation")
return err
}
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://%s.semaphoreci.com/api/v1alpha/yaml", org), bytes.NewBuffer(data))
if err != nil {
logrus.WithError(err).Error("failed to create request for yaml validation")
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
resp, err := http.DefaultClient.Do(req)
if err != nil {
logrus.WithError(err).Error("failed to make request for yaml validation")
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to validate YAML: %s", resp.Status)
}
result := map[string]interface{}{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
logrus.WithError(err).Error("failed to decode response for yaml validation")
return err
}
logrus.Debug(result["message"].(string))
return nil
}

func main() {
flag.Parse()
if debug {
logrus.SetLevel(logrus.DebugLevel)
}
if org == "" {
logrus.Fatal("Semaphore organization is required, set the SEMAPHORE_ORGANIZATION environment variable or use the -org flag")
}
if token == "" {
if os.Getenv("SEMAPHORE_API_TOKEN") == "" {
logrus.Fatal("Semaphore API token is required, set the SEMAPHORE_API_TOKEN environment variable or use the -token flag")
} else {
token = os.Getenv("SEMAPHORE_API_TOKEN")
}
}
var yamlFiles []string
if file != "" {
yamlFiles = strings.Split(file, ",")
}
if dir != "" {
semaphoreDirs := strings.Split(dir, ",")
logrus.WithField("semaphoreDirs", semaphoreDirs).Debug("looking for pipeline YAML files")
for _, semaphoreDir := range semaphoreDirs {
files, err := getPipelineYAMLFiles(semaphoreDir, strings.Split(skipDir, ","))
if err != nil {
logrus.WithError(err).Errorf("failed to get YAML files in %s", semaphoreDir)
continue
}
yamlFiles = append(yamlFiles, files...)
}
}
if len(yamlFiles) == 0 {
logrus.Error("no YAML files found")
return
}
logrus.Infof("will validate %d YAML pipeline file(s)", len(yamlFiles))
var failedFiles []string
for _, file := range yamlFiles {
err := validateYAML(file, org, token)
if err != nil {
logrus.WithError(err).Error("invalid YAML definition")
failedFiles = append(failedFiles, file)
}
}
if len(failedFiles) > 0 {
logrus.Fatalf("failed to validate %d files", len(failedFiles))
}
}