Skip to content

Commit

Permalink
Add parsing of user input
Browse files Browse the repository at this point in the history
Signed-off-by: Razieh Behjati <razieh@google.com>
  • Loading branch information
rbehjati committed Dec 21, 2022
1 parent 25fa0f3 commit d902fc6
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 31 deletions.
43 changes: 14 additions & 29 deletions internal/builders/docker/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand All @@ -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
}
4 changes: 2 additions & 2 deletions internal/builders/docker/pkg/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
169 changes: 169 additions & 0 deletions internal/builders/docker/pkg/config.go
Original file line number Diff line number Diff line change
@@ -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
}
70 changes: 70 additions & 0 deletions internal/builders/docker/pkg/config_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
40 changes: 40 additions & 0 deletions internal/builders/docker/pkg/options.go
Original file line number Diff line number Diff line change
@@ -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.")
}
4 changes: 4 additions & 0 deletions internal/builders/docker/testdata/config.toml
Original file line number Diff line number Diff line change
@@ -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"

0 comments on commit d902fc6

Please sign in to comment.