From d902fc65c2337b40cc28e9bcad2b7a7205015cba Mon Sep 17 00:00:00 2001 From: Razieh Behjati Date: Thu, 15 Dec 2022 15:22:19 +0000 Subject: [PATCH] Add parsing of user input Signed-off-by: Razieh Behjati --- internal/builders/docker/commands.go | 43 ++--- internal/builders/docker/pkg/common_test.go | 4 +- internal/builders/docker/pkg/config.go | 169 ++++++++++++++++++ internal/builders/docker/pkg/config_test.go | 70 ++++++++ internal/builders/docker/pkg/options.go | 40 +++++ internal/builders/docker/testdata/config.toml | 4 + 6 files changed, 299 insertions(+), 31 deletions(-) create mode 100644 internal/builders/docker/pkg/config.go create mode 100644 internal/builders/docker/pkg/config_test.go create mode 100644 internal/builders/docker/pkg/options.go create mode 100644 internal/builders/docker/testdata/config.toml diff --git a/internal/builders/docker/commands.go b/internal/builders/docker/commands.go index 532f3420db..f58c367083 100644 --- a/internal/builders/docker/commands.go +++ b/internal/builders/docker/commands.go @@ -27,45 +27,26 @@ import ( "github.com/spf13/cobra" ) -// InputOptions are the common options for the dry run and build command. -type InputOptions struct { - BuildConfigPath string - SourceRepo string - GitCommitHash string - BuilderImage string -} - -// AddFlags adds input flags to the given command. -func (o *InputOptions) AddFlags(cmd *cobra.Command) { - cmd.Flags().StringVarP(&o.BuildConfigPath, "build-config-path", "c", "", - "Required - Path to a toml file containing the build configs.") - - cmd.Flags().StringVarP(&o.SourceRepo, "source-repo", "s", "", - "Required - URL of the source repo.") - - cmd.Flags().StringVarP(&o.GitCommitHash, "git-commit-hash", "g", "", - "Required - SHA1 Git commit digest of the revision of the source code to build the artefact from.") - - cmd.Flags().StringVarP(&o.BuilderImage, "builder-image", "b", "", - "Required - URL indicating the Docker builder image, including a URI and image digest.") -} - // DryRunCmd validates the input flags, generates a BuildDefinition from them. func DryRunCmd(check func(error)) *cobra.Command { - o := &InputOptions{} + io := &pkg.InputOptions{} var buildDefinitionPath string cmd := &cobra.Command{ Use: "dry-run [FLAGS]", Short: "Generates and stores a JSON-formatted BuildDefinition based on the input arguments.", Run: func(cmd *cobra.Command, args []string) { - // TODO(#1191): Parse the input arguments into an instance of BuildDefinition. + config, err := pkg.NewDockerBuildConfig(io) + check(err) + log.Printf("The config is: %v\n", config) + + // TODO(#1191): Create an instance of BuildDefinition from config. bd := &pkg.BuildDefinition{} check(writeBuildDefinitionToFile(*bd, buildDefinitionPath)) }, } - o.AddFlags(cmd) + io.AddFlags(cmd) cmd.Flags().StringVarP(&buildDefinitionPath, "build-definition-path", "o", "", "Required - Path to store the generated BuildDefinition to.") @@ -87,20 +68,24 @@ func writeBuildDefinitionToFile(bd pkg.BuildDefinition, path string) error { // BuildCmd builds the artifacts using the input flags, and prints out their digests, or exists with an error. func BuildCmd(check func(error)) *cobra.Command { - o := &InputOptions{} + io := &pkg.InputOptions{} cmd := &cobra.Command{ Use: "build [FLAGS]", Short: "Builds the artifacts using the build config, source repo, and the builder image.", Run: func(cmd *cobra.Command, args []string) { - // TODO(#1191): Set up build state and build the artifact. + config, err := pkg.NewDockerBuildConfig(io) + check(err) + log.Printf("The config is: %v\n", config) + + // TODO(#1191): Set up build state using config, and build the artifact. artifacts := "To be implemented" log.Printf("Generated artifacts are: %v\n", artifacts) // TODO(#1191): Write subjects to file. }, } - o.AddFlags(cmd) + io.AddFlags(cmd) return cmd } diff --git a/internal/builders/docker/pkg/common_test.go b/internal/builders/docker/pkg/common_test.go index 582bf45e12..81a398b7b9 100644 --- a/internal/builders/docker/pkg/common_test.go +++ b/internal/builders/docker/pkg/common_test.go @@ -52,7 +52,7 @@ func Test_BuildDefinition(t *testing.T) { }, } - if !cmp.Equal(got, want) { - t.Errorf(cmp.Diff(got, want)) + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf(diff) } } diff --git a/internal/builders/docker/pkg/config.go b/internal/builders/docker/pkg/config.go new file mode 100644 index 0000000000..63411b2c05 --- /dev/null +++ b/internal/builders/docker/pkg/config.go @@ -0,0 +1,169 @@ +// Copyright 2022 SLSA Authors +// +// 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 +// +// https://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 pkg + +// This file contains functionality and structs for validating and +// representing user inputs and configuration files. + +import ( + "fmt" + "net/url" + "strings" + + toml "github.com/pelletier/go-toml" + "github.com/slsa-framework/slsa-github-generator/internal/utils" +) + +// BuildConfig is a collection of parameters to use for building the artifact. +type BuildConfig struct { + // TODO(#1191): Add env and options if needed. + // Command to pass to `docker run`. The command is taken as an array + // instead of a single string to avoid unnecessary parsing. See + // https://docs.docker.com/engine/reference/builder/#cmd and + // https://man7.org/linux/man-pages/man3/exec.3.html for more details. + Command []string `toml:"command"` + + // The path, relative to the root of the git repository, where the artifact + // built by the `docker run` command is expected to be found. + ArtifactPath string `toml:"artifact_path"` +} + +// Digest specifies a digest values, including the name of the hash function +// that was used for computing the digest. +type Digest struct { + Alg string + Value string +} + +// DockerImage fully specifies a docker image by a URI (e.g., including the +// docker image name and registry), and its digest. +type DockerImage struct { + URI string + Digest Digest +} + +// ToString returns the builder image in the form of NAME@ALG:VALUE. +func (bi *DockerImage) ToString() string { + return fmt.Sprintf("%s@%s:%s", bi.URI, bi.Digest.Alg, bi.Digest.Value) +} + +// DockerBuildConfig is a convenience class for holding validated user inputs. +type DockerBuildConfig struct { + SourceRepo string + SourceDigest Digest + BuilderImage DockerImage + BuildConfigPath string +} + +// NewDockerBuildConfig validates the inputs and generates an instance of +// DockerBuildConfig. +func NewDockerBuildConfig(io *InputOptions) (*DockerBuildConfig, error) { + if err := validateURI(io.SourceRepo); err != nil { + return nil, err + } + + sourceRepoDigest, err := validateDigest(io.GitCommitHash) + if err != nil { + return nil, err + } + + dockerImage, err := validateDockerImage(io.BuilderImage) + if err != nil { + return nil, err + } + + if err = validatePath(io.BuildConfigPath); err != nil { + return nil, fmt.Errorf("invalid build config path: %v", err) + } + + return &DockerBuildConfig{ + SourceRepo: io.SourceRepo, + SourceDigest: *sourceRepoDigest, + BuilderImage: *dockerImage, + BuildConfigPath: io.BuildConfigPath, + }, nil +} + +func validateURI(input string) error { + _, err := url.Parse(input) + if err != nil { + return fmt.Errorf("could not parse string (%q) as URI: %v", input, err) + } + return nil +} + +func validateDigest(input string) (*Digest, error) { + // We expect the input to be of the form ALG:VALUE + parts := strings.Split(input, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("got %s, want ALG:VALUE format", input) + } + digest := Digest{ + Alg: parts[0], + Value: parts[1], + } + return &digest, nil +} + +func validateDockerImage(image string) (*DockerImage, error) { + imageParts := strings.Split(image, "@") + if len(imageParts) != 2 { + return nil, fmt.Errorf("got %s, want NAME@DIGEST format", image) + } + + if err := validateURI(imageParts[0]); err != nil { + return nil, fmt.Errorf("docker image name (%q) is not a valid URI: %v", imageParts[0], err) + } + + digest, err := validateDigest(imageParts[1]) + if err != nil { + return nil, fmt.Errorf("docker image digest (%q) is malformed: %v", imageParts[1], err) + } + + dockerImage := DockerImage{ + URI: imageParts[0], + Digest: *digest, + } + + return &dockerImage, nil +} + +func validatePath(path string) error { + err := utils.PathIsUnderCurrentDirectory(path) + if err != nil { + return fmt.Errorf("path (%q) is not in the current directory", path) + } + return nil +} + +// ToMap returns this instance as a mapping between the algorithm and value. +func (d *Digest) ToMap() map[string]string { + return map[string]string{d.Alg: d.Value} +} + +// LoadBuildConfigFromFile loads build configuration from a toml file in the given path and returns an instance of BuildConfig. +func LoadBuildConfigFromFile(path string) (*BuildConfig, error) { + tomlTree, err := toml.LoadFile(path) + if err != nil { + return nil, fmt.Errorf("couldn't load toml file: %v", err) + } + + config := BuildConfig{} + if err := tomlTree.Unmarshal(&config); err != nil { + return nil, fmt.Errorf("couldn't ubmarshal toml file: %v", err) + } + + return &config, nil +} diff --git a/internal/builders/docker/pkg/config_test.go b/internal/builders/docker/pkg/config_test.go new file mode 100644 index 0000000000..1b2b40ff38 --- /dev/null +++ b/internal/builders/docker/pkg/config_test.go @@ -0,0 +1,70 @@ +// Copyright 2022 SLSA Authors +// +// 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 +// +// https://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 pkg + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_LoadBuildConfigFromFile(t *testing.T) { + got, err := LoadBuildConfigFromFile("../testdata/config.toml") + if err != nil { + t.Fatalf("couldn't load config file: %v", err) + } + + want := BuildConfig{ + Command: []string{"cp", "internal/builders/docker/testdata/config.toml", "config.toml"}, + ArtifactPath: "config.toml", + } + + if diff := cmp.Diff(*got, want); diff != "" { + t.Errorf(diff) + } +} + +func Test_NewDockerBuildConfig(t *testing.T) { + io := &InputOptions{ + BuildConfigPath: "testdata/config.toml", + SourceRepo: "https://github.com/project-oak/transparent-release", + GitCommitHash: "sha1:9b5f98310dbbad675834474fa68c37d880687cb9", + BuilderImage: "bash@sha256:9e2ba52487d945504d250de186cb4fe2e3ba023ed2921dd6ac8b97ed43e76af9", + } + got, err := NewDockerBuildConfig(io) + if err != nil { + t.Fatalf("invalid inputs: %v", err) + } + + want := DockerBuildConfig{ + SourceRepo: io.SourceRepo, + SourceDigest: Digest{ + Alg: "sha1", + Value: "9b5f98310dbbad675834474fa68c37d880687cb9", + }, + BuilderImage: DockerImage{ + URI: "bash", + Digest: Digest{ + Alg: "sha256", + Value: "9e2ba52487d945504d250de186cb4fe2e3ba023ed2921dd6ac8b97ed43e76af9", + }, + }, + BuildConfigPath: io.BuildConfigPath, + } + + if diff := cmp.Diff(*got, want); diff != "" { + t.Errorf(diff) + } +} diff --git a/internal/builders/docker/pkg/options.go b/internal/builders/docker/pkg/options.go new file mode 100644 index 0000000000..c4cd5128cf --- /dev/null +++ b/internal/builders/docker/pkg/options.go @@ -0,0 +1,40 @@ +// Copyright 2022 SLSA Authors +// +// 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 +// +// https://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 pkg + +import "github.com/spf13/cobra" + +// InputOptions are the common options for the dry run and build command. +type InputOptions struct { + BuildConfigPath string + SourceRepo string + GitCommitHash string + BuilderImage string +} + +// AddFlags adds input flags to the given command. +func (io *InputOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&io.BuildConfigPath, "build-config-path", "c", "", + "Required - Path to a toml file containing the build configs.") + + cmd.Flags().StringVarP(&io.SourceRepo, "source-repo", "s", "", + "Required - URL of the source repo.") + + cmd.Flags().StringVarP(&io.GitCommitHash, "git-commit-hash", "g", "", + "Required - SHA1 Git commit digest of the revision of the source code to build the artefact from.") + + cmd.Flags().StringVarP(&io.BuilderImage, "builder-image", "b", "", + "Required - URL indicating the Docker builder image, including a URI and image digest.") +} diff --git a/internal/builders/docker/testdata/config.toml b/internal/builders/docker/testdata/config.toml new file mode 100644 index 0000000000..05b36abcea --- /dev/null +++ b/internal/builders/docker/testdata/config.toml @@ -0,0 +1,4 @@ +# Simple command for generating a file. +command = ["cp", "internal/builders/docker/testdata/config.toml", "config.toml"] +# Path to the file generated by the command above. +artifact_path = "config.toml"