diff --git a/cmd/job-executor-service/main.go b/cmd/job-executor-service/main.go index 11a8f735..d91b87c8 100644 --- a/cmd/job-executor-service/main.go +++ b/cmd/job-executor-service/main.go @@ -46,6 +46,8 @@ type envConfig struct { DefaultResourceRequestsMemory string `envconfig:"DEFAULT_RESOURCE_REQUESTS_MEMORY"` // Respond with .finished event if no configuration found AlwaysSendFinishedEvent string `envconfig:"ALWAYS_SEND_FINISHED_EVENT"` + // Container Registry + ContainerRegistry string `envconfig:"CONTAINER_REGISTRY"` } // ServiceName specifies the current services name (e.g., used as source when sending CloudEvents) @@ -104,6 +106,7 @@ func processKeptnCloudEvent(ctx context.Context, event cloudevents.Event) error InitContainerImage: env.InitContainerImage, DefaultResourceRequirements: DefaultResourceRequirements, AlwaysSendFinishedEvent: false, + ContainerRegistry: env.ContainerRegistry, }, } diff --git a/deploy/registry.yaml b/deploy/registry.yaml new file mode 100644 index 00000000..eb4c3bf3 --- /dev/null +++ b/deploy/registry.yaml @@ -0,0 +1,44 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + creationTimestamp: null + labels: + app: registry + name: registry +spec: + replicas: 1 + selector: + matchLabels: + app: registry + strategy: {} + template: + metadata: + creationTimestamp: null + labels: + app: registry + spec: + containers: + - image: registry:2 + name: registry + ports: + - containerPort: 5000 + resources: {} +status: {} + +--- +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: null + labels: + app: registry + name: registry +spec: + ports: + - port: 5000 + protocol: TCP + targetPort: 5000 + selector: + app: registry +status: + loadBalancer: {} diff --git a/deploy/service.yaml b/deploy/service.yaml index 98b7fba0..a441aab1 100644 --- a/deploy/service.yaml +++ b/deploy/service.yaml @@ -46,6 +46,8 @@ spec: value: "50m" - name: DEFAULT_RESOURCE_REQUESTS_MEMORY value: "128Mi" + - name: CONTAINER_REGISTRY + value: "TODO" - name: distributor image: keptn/distributor:0.8.4 livenessProbe: diff --git a/go.mod b/go.mod index 4c8a33ff..a845e49a 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( golang.org/x/term v0.0.0-20210429154555-c04ba851c2a4 // indirect golang.org/x/tools v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b gotest.tools v2.2.0+incompatible honnef.co/go/tools v0.1.3 // indirect k8s.io/api v0.22.0 diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index a5a7e272..ae8f9b78 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -9,3 +9,4 @@ data: default_resource_limits_memory: "512Mi" default_resource_requests_cpu: "50m" default_resource_requests_memory: "128Mi" + container_registry: "TODO" diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 8d8bd5be..e324129c 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -82,6 +82,11 @@ spec: configMapKeyRef: name: job-service-config key: default_resource_requests_memory + - name: CONTAINER_REGISTRY + valueFrom: + configMapKeyRef: + name: job-service-config + key: container_registry livenessProbe: httpGet: path: /health diff --git a/pkg/config/config.go b/pkg/config/config.go index 0653cf5a..84265db9 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,6 +3,7 @@ package config import ( "fmt" "gopkg.in/yaml.v2" + "keptn-sandbox/job-executor-service/pkg/github/model" "regexp" "github.com/PaesslerAG/jsonpath" @@ -18,10 +19,11 @@ type Config struct { // Action contains a action within the config which needs to be triggered type Action struct { - Name string `yaml:"name"` - Events []Event `yaml:"events"` - Tasks []Task `yaml:"tasks"` - Silent bool `yaml:"silent"` + Name string `yaml:"name"` + Events []Event `yaml:"events"` + Tasks []Task `yaml:"tasks"` + Silent bool `yaml:"silent"` + Steps []model.Step `yaml:"steps"` } // Event defines a keptn event which determines if an Action should be triggered @@ -85,7 +87,20 @@ func NewConfig(yamlContent []byte) (*Config, error) { return nil, fmt.Errorf("apiVersion %v is not supported, use %v", *config.APIVersion, supportedAPIVersion) } - return &config, nil + err = config.checkConfigs() + + return &config, err +} + +func (c *Config) checkConfigs() error { + + // check if the actions don't contain steps and tasks -> that should not be valid + for _, action := range c.Actions { + if len(action.Steps) > 0 && len(action.Tasks) > 0 { + return fmt.Errorf("action %v contains steps and tasks", action.Name) + } + } + return nil } // IsEventMatch indicated whether a given event matches the config diff --git a/pkg/eventhandler/eventhandler_test.go b/pkg/eventhandler/eventhandler_test.go index 8270a4ac..2210412f 100644 --- a/pkg/eventhandler/eventhandler_test.go +++ b/pkg/eventhandler/eventhandler_test.go @@ -196,3 +196,10 @@ func TestStartK8sJobSilent(t *testing.T) { err = fakeEventSender.AssertSentEventTypes([]string{}) assert.NilError(t, err) } + +func TestGetGithubProjectName(t *testing.T) { + + eh := EventHandler{} + assert.Equal(t, "aquasecurity/trivy-action", eh.getGithubProjectName("aquasecurity/trivy-action@master")) + assert.Equal(t, "aquasecurity/trivy-action", eh.getGithubProjectName("aquasecurity/trivy-action")) +} \ No newline at end of file diff --git a/pkg/eventhandler/eventhandlers.go b/pkg/eventhandler/eventhandlers.go index c0a574e5..9f810940 100644 --- a/pkg/eventhandler/eventhandlers.go +++ b/pkg/eventhandler/eventhandlers.go @@ -4,10 +4,12 @@ import ( "encoding/json" "fmt" "keptn-sandbox/job-executor-service/pkg/config" + "keptn-sandbox/job-executor-service/pkg/github" "keptn-sandbox/job-executor-service/pkg/k8sutils" "log" "math" "strconv" + "strings" cloudevents "github.com/cloudevents/sdk-go/v2" // make sure to use v2 cloudevents here keptnv2 "github.com/keptn/go-utils/pkg/lib/v0_2_0" @@ -73,13 +75,88 @@ func (eh *EventHandler) HandleEvent() error { } log.Printf("Match found for event %s of type %s. Starting k8s job to run action '%s'", eh.Event.Context.GetID(), eh.Event.Type(), action.Name) + log.Printf("action: %+v", *action) k8s := k8sutils.NewK8s(eh.JobSettings.JobNamespace) + err = k8s.ConnectToCluster() + if err != nil { + log.Printf("Error while connecting to cluster: %s\n", err) + return err + } + + err = eh.handleGithubAction(k8s, action) + if err != nil { + log.Printf("An error occurred while handling GitHub action: %s", err) + return err + } + + log.Printf("executing action: %+v", *action) eh.startK8sJob(k8s, action, eventAsInterface) return nil } +func (eh *EventHandler) handleGithubAction(k8s k8sutils.K8s, action *config.Action) error { + for _, step := range action.Steps { + + log.Printf("Handling step: %+v", step) + + githubProjectName := eh.getGithubProjectName(step.Uses) + + // TODO using step.Name here is a bad idea because it can contain whitespaces etc. + imageLocation, err := k8s.CreateImageBuilder(step.Name, githubProjectName, eh.JobSettings.ContainerRegistry) + log.Printf("imageLocation: %v", imageLocation) + if err != nil { + return err + } + + defer func() { + err = k8s.DeleteK8sJob(step.Name) + if err != nil { + log.Printf("Error while deleting job: %s\n", err.Error()) + } + }() + + jobErr := k8s.AwaitK8sJobDone(step.Name, defaultMaxPollCount, pollIntervalInSeconds) + if jobErr != nil { + log.Println(err) + } + + err, githubAction := github.GetActionYaml(githubProjectName) + if err != nil { + return err + } + + args, err := github.PrepareArgs(step.With, githubAction.Inputs, githubAction.Runs.Args) + if err != nil { + return err + } + + task := config.Task{ + Name: githubAction.Name, + Files: nil, + Image: imageLocation, + Cmd: nil, + Args: args, + Env: nil, + } + + log.Printf("task: %+v", task) + + action.Tasks = append(action.Tasks, task) + } + return nil +} + +func (eh *EventHandler) getGithubProjectName(uses string) string { + githubProjectName := uses + index := strings.LastIndex(githubProjectName, "@") + if index > 0 { + githubProjectName = githubProjectName[:index] + } + return githubProjectName +} + func (eh *EventHandler) createEventPayloadAsInterface() (map[string]interface{}, error) { var eventDataAsInterface interface{} @@ -114,15 +191,6 @@ func (eh *EventHandler) startK8sJob(k8s k8sutils.K8s, action *config.Action, jso } } - err := k8s.ConnectToCluster() - if err != nil { - log.Printf("Error while connecting to cluster: %s\n", err.Error()) - if !action.Silent { - sendTaskFailedEvent(eh.Keptn, "", eh.ServiceName, err, "") - } - return - } - allJobLogs := []jobLogs{} for index, task := range action.Tasks { diff --git a/pkg/file/file.go b/pkg/file/file.go index 1f55754e..5bd0121f 100644 --- a/pkg/file/file.go +++ b/pkg/file/file.go @@ -27,6 +27,11 @@ func MountFiles(actionName string, taskName string, fs afero.Fs, configService k return fmt.Errorf("no action found with name '%s'", actionName) } + if len(action.Steps) >= 0 { + log.Printf("mounting files for github actions is not supported") + return nil + } + found, task := action.FindTaskByName(taskName) if !found { return fmt.Errorf("no task found with name '%s'", taskName) diff --git a/pkg/github/github.go b/pkg/github/github.go new file mode 100644 index 00000000..a7c85d17 --- /dev/null +++ b/pkg/github/github.go @@ -0,0 +1,36 @@ +package github + +import ( + "fmt" + "keptn-sandbox/job-executor-service/pkg/github/model" + "strings" +) + +func PrepareArgs(with map[string]string, inputs map[string]model.Input, args []string) ([]string, error) { + var filledArgs []string + + for inputKey, inputValue := range inputs { + argKey := fmt.Sprintf("inputs.%s", inputKey) + + for _, arg := range args { + if strings.Contains(arg, argKey) { + + argValue := inputValue.Default + if withValue, ok := with[inputKey]; ok { + argValue = withValue + } else { + if inputValue.Required { + return nil, fmt.Errorf("required input '%s' not provided", inputKey) + } + } + + splittedArg := strings.Split(arg, "$") + arg := strings.TrimSpace(splittedArg[0]) + filledArgs = append(filledArgs, arg) + filledArgs = append(filledArgs, argValue) + } + } + } + + return filledArgs, nil +} diff --git a/pkg/github/github_test.go b/pkg/github/github_test.go new file mode 100644 index 00000000..c2d34932 --- /dev/null +++ b/pkg/github/github_test.go @@ -0,0 +1,43 @@ +package github + +import ( + "gotest.tools/assert" + "keptn-sandbox/job-executor-service/pkg/github/model" + "log" + "testing" +) + +func TestPrepareArgs(t *testing.T) { + with := map[string]string{"scan-type": "banana", "format": "cucumber"} + inputs := map[string]model.Input{ + "scan-type": { + Required: false, + Default: "", + }, + "format": { + Required: false, + Default: "", + }, + "template": { + Required: false, + Default: "table", + }, + } + args := []string{"-a ${{ inputs.scan-type }}", "-b ${{ inputs.format }}", "-c ${{ inputs.template }}"} + k8sArgs, err := PrepareArgs(with, inputs, args) + assert.NilError(t, err) + log.Printf("%v", k8sArgs) +} + +func TestPrepareArgs_RequiredInput(t *testing.T) { + with := map[string]string{} + inputs := map[string]model.Input{ + "scan-type": { + Required: true, + Default: "", + }, + } + args := []string{"-a ${{ inputs.scan-type }}"} + _, err := PrepareArgs(with, inputs, args) + assert.Error(t, err, "required input 'scan-type' not provided") +} diff --git a/pkg/github/model/action.go b/pkg/github/model/action.go new file mode 100644 index 00000000..55a68491 --- /dev/null +++ b/pkg/github/model/action.go @@ -0,0 +1,63 @@ +package model + +import ( + "gopkg.in/yaml.v3" + "io" +) + +// ActionRunsUsing is the type of runner for the action +type ActionRunsUsing string + +const ( + // ActionRunsUsingNode12 for running with node12 + ActionRunsUsingNode12 = "node12" + // ActionRunsUsingDocker for running with docker + ActionRunsUsingDocker = "docker" + // ActionRunsUsingComposite for running composite + ActionRunsUsingComposite = "composite" +) + +// ActionRuns are a field in Action +type ActionRuns struct { + Using ActionRunsUsing `yaml:"using"` + Env map[string]string `yaml:"env"` + Main string `yaml:"main"` + Image string `yaml:"image"` + Entrypoint []string `yaml:"entrypoint"` + Args []string `yaml:"args"` + Steps []Step `yaml:"steps"` +} + +// Action describes a metadata file for GitHub actions. The metadata filename must be either action.yml or action.yaml. The data in the metadata file defines the inputs, outputs and main entrypoint for your action. +type Action struct { + Name string `yaml:"name"` + Author string `yaml:"author"` + Description string `yaml:"description"` + Inputs map[string]Input `yaml:"inputs"` + Outputs map[string]Output `yaml:"outputs"` + Runs ActionRuns `yaml:"runs"` + Branding struct { + Color string `yaml:"color"` + Icon string `yaml:"icon"` + } `yaml:"branding"` +} + +// Input parameters allow you to specify data that the action expects to use during runtime. GitHub stores input parameters as environment variables. Input ids with uppercase letters are converted to lowercase during runtime. We recommended using lowercase input ids. +type Input struct { + Description string `yaml:"description"` + Required bool `yaml:"required"` + Default string `yaml:"default"` +} + +// Output parameters allow you to declare data that an action sets. Actions that run later in a workflow can use the output data set in previously run actions. For example, if you had an action that performed the addition of two inputs (x + y = z), the action could output the sum (z) for other actions to use as an input. +type Output struct { + Description string `yaml:"description"` + Value string `yaml:"value"` +} + +// ReadAction reads an action from a reader +func ReadAction(in io.Reader) (*Action, error) { + a := new(Action) + err := yaml.NewDecoder(in).Decode(a) + return a, err +} diff --git a/pkg/github/model/workflow.go b/pkg/github/model/workflow.go new file mode 100644 index 00000000..d496c9af --- /dev/null +++ b/pkg/github/model/workflow.go @@ -0,0 +1,78 @@ +package model + +import ( + "gopkg.in/yaml.v3" +) + +// Workflow is the structure of the files in .github/workflows +type Workflow struct { + Name string `yaml:"name"` + RawOn yaml.Node `yaml:"on"` + Env map[string]string `yaml:"env"` + Jobs map[string]*Job `yaml:"jobs"` + Defaults Defaults `yaml:"defaults"` +} + +// Job is the structure of one job in a workflow +type Job struct { + Name string `yaml:"name"` + RawNeeds yaml.Node `yaml:"needs"` + RawRunsOn yaml.Node `yaml:"runs-on"` + Env yaml.Node `yaml:"env"` + If yaml.Node `yaml:"if"` + Steps []*Step `yaml:"steps"` + TimeoutMinutes int64 `yaml:"timeout-minutes"` + Services map[string]*ContainerSpec `yaml:"services"` + Strategy *Strategy `yaml:"strategy"` + RawContainer yaml.Node `yaml:"container"` + Defaults Defaults `yaml:"defaults"` + Outputs map[string]string `yaml:"outputs"` +} + +// Strategy for the job +type Strategy struct { + FailFast bool + MaxParallel int + FailFastString string `yaml:"fail-fast"` + MaxParallelString string `yaml:"max-parallel"` + RawMatrix yaml.Node `yaml:"matrix"` +} + +// Defaults settings that will apply to all steps in the job or workflow +type Defaults struct { + Run RunDefaults `yaml:"run"` +} + +// RunDefaults for all run steps in the job or workflow +type RunDefaults struct { + Shell string `yaml:"shell"` + WorkingDirectory string `yaml:"working-directory"` +} + +// ContainerSpec is the specification of the container to use for the job +type ContainerSpec struct { + Image string `yaml:"image"` + Env map[string]string `yaml:"env"` + Ports []string `yaml:"ports"` + Volumes []string `yaml:"volumes"` + Options string `yaml:"options"` + Entrypoint string + Args string + Name string + Reuse bool +} + +// Step is the structure of one step in a job +type Step struct { + ID string `yaml:"id"` + If yaml.Node `yaml:"if"` + Name string `yaml:"name"` + Uses string `yaml:"uses"` + Run string `yaml:"run"` + WorkingDirectory string `yaml:"working-directory"` + Shell string `yaml:"shell"` + Env yaml.Node `yaml:"env"` + With map[string]string `yaml:"with"` + ContinueOnError bool `yaml:"continue-on-error"` + TimeoutMinutes int64 `yaml:"timeout-minutes"` +} diff --git a/pkg/github/reader.go b/pkg/github/reader.go new file mode 100644 index 00000000..a531f971 --- /dev/null +++ b/pkg/github/reader.go @@ -0,0 +1,48 @@ +package github + +import ( + "fmt" + "gopkg.in/yaml.v2" + "io/ioutil" + "keptn-sandbox/job-executor-service/pkg/github/model" + "net/http" +) + +func GetActionYaml(githubRepoName string) (error, *model.Action) { + + action := &model.Action{} + + err, actionAsString := readActionYamlFromGithub(githubRepoName) + if err != nil { + return err, action + } + + err = yaml.Unmarshal(actionAsString, action) + if err != nil { + return err, action + } + + return nil, action +} + +func readActionYamlFromGithub(githubRepoName string) (error, []byte) { + + // e.g. https://raw.githubusercontent.com/aquasecurity/trivy-action/master/action.yaml + response, err := http.Get("https://raw.githubusercontent.com/" + githubRepoName + "/master/action.yaml") + + if err != nil { + return err, nil + } + + defer response.Body.Close() + + if response.StatusCode == http.StatusOK { + bodyBytes, err := ioutil.ReadAll(response.Body) + if err != nil { + return err, nil + } + return nil, bodyBytes + } + + return fmt.Errorf("HTTP status code was: %v", response.StatusCode), nil +} diff --git a/pkg/github/reader_test.go b/pkg/github/reader_test.go new file mode 100644 index 00000000..56bbe9d0 --- /dev/null +++ b/pkg/github/reader_test.go @@ -0,0 +1,113 @@ +package github + +import ( + "gotest.tools/assert" + "testing" +) + +// For a POC is this ok ... kinda: +const trivyActionFile string = `name: 'Aqua Security Trivy' +description: 'Scans container images for vulnerabilities with Trivy' +author: 'Aqua Security' +inputs: + scan-type: + description: 'Scan type to use for scanning vulnerability' + required: false + default: 'image' + image-ref: + description: 'image reference(for backward compatibility)' + required: true + input: + description: 'reference of tar file to scan' + required: false + default: '' + scan-ref: + description: 'Scan reference' + required: false + default: '.' + exit-code: + description: 'exit code when vulnerabilities were found' + required: false + default: '0' + ignore-unfixed: + description: 'ignore unfixed vulnerabilities' + required: false + default: 'false' + vuln-type: + description: 'comma-separated list of vulnerability types (os,library)' + required: false + default: 'os,library' + severity: + description: 'severities of vulnerabilities to be displayed' + required: false + default: 'UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL' + format: + description: 'output format (table, json, template)' + required: false + default: 'table' + template: + description: 'use an existing template for rendering output (@/contrib/sarif.tpl, @/contrib/gitlab.tpl, @/contrib/junit.tpl' + required: false + default: '' + output: + description: 'writes results to a file with the specified file name' + required: false + default: '' + skip-dirs: + description: 'comma separated list of directories where traversal is skipped' + required: false + default: '' + cache-dir: + description: 'specify where the cache is stored' + required: false + default: '' + timeout: + description: 'timeout (default 2m0s)' + required: false + default: '' + ignore-policy: + description: 'filter vulnerabilities with OPA rego language' + required: false + default: '' + hide-progress: + description: 'hide progress output' + required: false + default: 'true' +runs: + using: 'docker' + image: "Dockerfile" + args: + - '-a ${{ inputs.scan-type }}' + - '-b ${{ inputs.format }}' + - '-c ${{ inputs.template }}' + - '-d ${{ inputs.exit-code }}' + - '-e ${{ inputs.ignore-unfixed }}' + - '-f ${{ inputs.vuln-type }}' + - '-g ${{ inputs.severity }}' + - '-h ${{ inputs.output }}' + - '-i ${{ inputs.image-ref }}' + - '-j ${{ inputs.scan-ref }}' + - '-k ${{ inputs.skip-dirs }}' + - '-l ${{ inputs.input }}' + - '-m ${{ inputs.cache-dir }}' + - '-n ${{ inputs.timeout }}' + - '-o ${{ inputs.ignore-policy }}' + - '-p ${{ inputs.hide-progress }}' +` + +func TestDownloadFileFromGithub(t *testing.T) { + + err, bytes := readActionYamlFromGithub("aquasecurity/trivy-action") + assert.NilError(t, err) + + assert.Equal(t, string(bytes), trivyActionFile) +} + +func TestUnmarshalActionYaml(t *testing.T) { + + err, action := GetActionYaml("aquasecurity/trivy-action") + + assert.NilError(t, err) + assert.Check(t, action != nil) + assert.Equal(t, action.Name, "Aqua Security Trivy") +} diff --git a/pkg/k8sutils/builder.go b/pkg/k8sutils/builder.go new file mode 100644 index 00000000..ce08fa02 --- /dev/null +++ b/pkg/k8sutils/builder.go @@ -0,0 +1,100 @@ +package k8sutils + +import ( + "context" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (k8s *k8sImpl) CreateImageBuilder(jobName string, githubProjectName string, registry string) (string, error) { + + var backOffLimit int32 = 0 + +/* convert := func(s int64) *int64 { + return &s + }*/ + + imageRegistryPath := registry + "/" + githubProjectName + + jobSpec := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: jobName, + Namespace: k8s.namespace, + }, + Spec: batchv1.JobSpec{ + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ +/* SecurityContext: &v1.PodSecurityContext{ + RunAsUser: convert(1000), + RunAsGroup: convert(2000), + FSGroup: convert(2000), + },*/ + Containers: []v1.Container{ + { + Name: jobName, + Image: "gcr.io/kaniko-project/executor:latest", + Env: []v1.EnvVar{ + { + Name: "GOOGLE_APPLICATION_CREDENTIALS", + Value: "/secret/config.json", + }, + }, + Args: []string{ + "--destination", + imageRegistryPath, + "--context", + "git://github.com/" + githubProjectName, + }, + VolumeMounts: []v1.VolumeMount{ + { + Name: "gcr-secret", + MountPath: "/secret", + }, + { + Name: "workspace", + MountPath: "/workspace", + }, + }, + }, + }, + RestartPolicy: v1.RestartPolicyNever, + Volumes: []v1.Volume{ + { + Name: "workspace", + VolumeSource: v1.VolumeSource{ + EmptyDir: &v1.EmptyDirVolumeSource{}, + }, + }, + { + Name: "gcr-secret", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: "kaniko", + Items: []v1.KeyToPath{ + { + Key: "config.json", + Path: "config.json", + Mode: nil, + }, + }, + }, + }, + }, + }, + }, + }, + BackoffLimit: &backOffLimit, + }, + } + + jobs := k8s.clientset.BatchV1().Jobs(k8s.namespace) + + _, err := jobs.Create(context.TODO(), jobSpec, metav1.CreateOptions{}) + + if err != nil { + return "", err + } + + return imageRegistryPath, nil +} diff --git a/pkg/k8sutils/builder_test.go b/pkg/k8sutils/builder_test.go new file mode 100644 index 00000000..bed08c23 --- /dev/null +++ b/pkg/k8sutils/builder_test.go @@ -0,0 +1,9 @@ +package k8sutils + +/*func TestBuilder(t *testing.T) { + + k8s := k8sImpl{} + image, err := k8s.CreateImageBuilder("whatever", model.Step{}, "whatever") + assert.NilError(t, err) + log.Printf("%v", image) +}*/ \ No newline at end of file diff --git a/pkg/k8sutils/connect.go b/pkg/k8sutils/connect.go index e3f5cc1d..f3dfa464 100644 --- a/pkg/k8sutils/connect.go +++ b/pkg/k8sutils/connect.go @@ -23,6 +23,7 @@ type K8s interface { AwaitK8sJobDone(jobName string, maxPollDuration int, pollIntervalInSeconds int) error DeleteK8sJob(jobName string) error GetLogsOfPod(jobName string) (string, error) + CreateImageBuilder(jobName string, githubProjectName string, registry string) (string, error) } // NewK8s creates and returns new K8s diff --git a/pkg/k8sutils/fake/connect_mock.go b/pkg/k8sutils/fake/connect_mock.go index 80fdded2..83baab5d 100644 --- a/pkg/k8sutils/fake/connect_mock.go +++ b/pkg/k8sutils/fake/connect_mock.go @@ -64,6 +64,21 @@ func (mr *MockK8sMockRecorder) ConnectToCluster() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectToCluster", reflect.TypeOf((*MockK8s)(nil).ConnectToCluster)) } +// CreateImageBuilder mocks base method. +func (m *MockK8s) CreateImageBuilder(jobName, githubProjectName, registry string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateImageBuilder", jobName, githubProjectName, registry) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateImageBuilder indicates an expected call of CreateImageBuilder. +func (mr *MockK8sMockRecorder) CreateImageBuilder(jobName, githubProjectName, registry interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateImageBuilder", reflect.TypeOf((*MockK8s)(nil).CreateImageBuilder), jobName, githubProjectName, registry) +} + // CreateK8sJob mocks base method. func (m *MockK8s) CreateK8sJob(jobName string, action *config.Action, task config.Task, eventData *v0_2_0.EventData, jobSettings k8sutils.JobSettings, jsonEventData interface{}) error { m.ctrl.T.Helper() diff --git a/pkg/k8sutils/job.go b/pkg/k8sutils/job.go index 1af586ce..71cb0863 100644 --- a/pkg/k8sutils/job.go +++ b/pkg/k8sutils/job.go @@ -32,6 +32,7 @@ type JobSettings struct { InitContainerImage string DefaultResourceRequirements *v1.ResourceRequirements AlwaysSendFinishedEvent bool + ContainerRegistry string } // CreateK8sJob creates a k8s job with the job-executor-service-initcontainer and the job image of the task @@ -68,10 +69,10 @@ func (k8s *k8sImpl) CreateK8sJob(jobName string, action *config.Action, task con } automountServiceAccountToken := false - runAsNonRoot := true + /*runAsNonRoot := true convert := func(s int64) *int64 { return &s - } + }*/ jobEnv, err := k8s.prepareJobEnv(task, eventData, jsonEventData) if err != nil { @@ -86,12 +87,12 @@ func (k8s *k8sImpl) CreateK8sJob(jobName string, action *config.Action, task con Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ - SecurityContext: &v1.PodSecurityContext{ + /*SecurityContext: &v1.PodSecurityContext{ RunAsUser: convert(1000), RunAsGroup: convert(2000), FSGroup: convert(2000), RunAsNonRoot: &runAsNonRoot, - }, + },*/ InitContainers: []v1.Container{ { Name: "init-" + jobName, diff --git a/test-data/action.yaml b/test-data/action.yaml new file mode 100644 index 00000000..19d2dff0 --- /dev/null +++ b/test-data/action.yaml @@ -0,0 +1,87 @@ +name: 'Aqua Security Trivy' +description: 'Scans container images for vulnerabilities with Trivy' +author: 'Aqua Security' +inputs: + scan-type: + description: 'Scan type to use for scanning vulnerability' + required: false + default: 'image' + image-ref: + description: 'image reference(for backward compatibility)' + required: true + input: + description: 'reference of tar file to scan' + required: false + default: '' + scan-ref: + description: 'Scan reference' + required: false + default: '.' + exit-code: + description: 'exit code when vulnerabilities were found' + required: false + default: '0' + ignore-unfixed: + description: 'ignore unfixed vulnerabilities' + required: false + default: 'false' + vuln-type: + description: 'comma-separated list of vulnerability types (os,library)' + required: false + default: 'os,library' + severity: + description: 'severities of vulnerabilities to be displayed' + required: false + default: 'UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL' + format: + description: 'output format (table, json, template)' + required: false + default: 'table' + template: + description: 'use an existing template for rendering output (@/contrib/sarif.tpl, @/contrib/gitlab.tpl, @/contrib/junit.tpl' + required: false + default: '' + output: + description: 'writes results to a file with the specified file name' + required: false + default: '' + skip-dirs: + description: 'comma separated list of directories where traversal is skipped' + required: false + default: '' + cache-dir: + description: 'specify where the cache is stored' + required: false + default: '' + timeout: + description: 'timeout (default 2m0s)' + required: false + default: '' + ignore-policy: + description: 'filter vulnerabilities with OPA rego language' + required: false + default: '' + hide-progress: + description: 'hide progress output' + required: false + default: 'true' +runs: + using: 'docker' + image: "Dockerfile" + args: + - '-a ${{ inputs.scan-type }}' + - '-b ${{ inputs.format }}' + - '-c ${{ inputs.template }}' + - '-d ${{ inputs.exit-code }}' + - '-e ${{ inputs.ignore-unfixed }}' + - '-f ${{ inputs.vuln-type }}' + - '-g ${{ inputs.severity }}' + - '-h ${{ inputs.output }}' + - '-i ${{ inputs.image-ref }}' + - '-j ${{ inputs.scan-ref }}' + - '-k ${{ inputs.skip-dirs }}' + - '-l ${{ inputs.input }}' + - '-m ${{ inputs.cache-dir }}' + - '-n ${{ inputs.timeout }}' + - '-o ${{ inputs.ignore-policy }}' + - '-p ${{ inputs.hide-progress }}' \ No newline at end of file diff --git a/test-data/github-action.yaml b/test-data/github-action.yaml new file mode 100644 index 00000000..334188df --- /dev/null +++ b/test-data/github-action.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +actions: + - name: Image Scanning + events: + - name: sh.keptn.event.scan.triggered + steps: + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: 'ghcr.io/podtato-head/podtato-main:v1-v1' + format: 'table' + exit-code: '1' + ignore-unfixed: 'true' + vuln-type: 'os,library' + severity: 'LOW,MEDIUM,HIGH,CRITICAL' \ No newline at end of file diff --git a/test-data/workflow.yaml b/test-data/workflow.yaml new file mode 100644 index 00000000..559af9d7 --- /dev/null +++ b/test-data/workflow.yaml @@ -0,0 +1,60 @@ +############################################################################ +# Build Container Images # +############################################################################ +container_build: + needs: [prepare_ci_run] + strategy: + matrix: + component: [ "left-leg", "right-leg", "left-arm", "right-arm", "hats", "main" ] + version: [ "v1", "v2", "v3", "v4" ] + runs-on: ubuntu-20.04 + env: + BRANCH: ${{ needs.prepare_ci_run.outputs.BRANCH }} + VERSION: ${{ needs.prepare_ci_run.outputs.VERSION }} + DATETIME: ${{ needs.prepare_ci_run.outputs.DATE }}${{ needs.prepare_ci_run.outputs.TIME }} + GIT_SHA: ${{ needs.prepare_ci_run.outputs.GIT_SHA }} + steps: + - name: Checkout Code + uses: actions/checkout@v2 + + - uses: sigstore/cosign-installer@main + with: + cosign-release: 'v1.0.0' + + - name: Login to GitHub Container Registry + if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release-*' }} + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.CR_PAT }} + + + - name: Lint + uses: golangci/golangci-lint-action@v2 + with: + version: v1.29 + working-directory: podtato-services/${{ matrix.component }} + + - name: Build + id: docker_build + uses: docker/build-push-action@v2 + with: + build-args: | + VERSION=${{ matrix.version }} + context: podtato-services/${{ matrix.component }}/. + push: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release-*' }} + file: podtato-services/${{ matrix.component }}/docker/Dockerfile + platforms: linux/amd64 + tags: | + ${{ env.CONTAINER_REGISTRY }}/podtato-head/podtato-${{ matrix.component }}:${{ matrix.version}}-latest-dev + ${{ env.CONTAINER_REGISTRY }}/podtato-head/podtato-${{ matrix.component }}:${{ matrix.version}}-${{ env.VERSION }} + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: '${{ env.CONTAINER_REGISTRY }}/podtato-head/podtato-${{ matrix.component }}:${{ matrix.version}}-${{ env.VERSION }}' + format: 'table' + exit-code: '1' + ignore-unfixed: '${{ env.TRIVY_IGNORE_UNFIXED }}' + vuln-type: '${{ env.TRIVY_VULN_TYPE }}' + severity: '${{ env.TRIVY_SEVERITY }}' \ No newline at end of file