diff --git a/client.go b/client.go index 9d214c663..02bc95953 100644 --- a/client.go +++ b/client.go @@ -17,16 +17,16 @@ const ( // DefaultRegistry through which containers of Functions will be shuttled. DefaultRegistry = "docker.io" - // DefaultRuntime is the language runtime for a new Function, including - // the template written and builder invoked on deploy. - DefaultRuntime = "node" - // DefaultTemplate is the default Function signature / environmental context // of the resultant function. All runtimes are expected to have at least // one implementation of each supported function signature. Currently that // includes an HTTP Handler ("http") and Cloud Events handler ("events") DefaultTemplate = "http" + // DefaultVersion is the initial value for string members whose implicit type + // is a semver. + DefaultVersion = "0.0.0" + // The name of the config directory within ~/.config (or configured location) configDirName = "func" ) @@ -386,9 +386,8 @@ func (c *Client) New(ctx context.Context, cfg Function) (err error) { c.progressListener.Stopping() }() - // Create local template - err = c.Create(cfg) - if err != nil { + // Create Function at path indidcated by Config + if err = c.Create(cfg); err != nil { return } @@ -428,50 +427,63 @@ func (c *Client) New(ctx context.Context, cfg Function) (err error) { return } -// Create a new Function project locally using the settings provided on a -// Function object. +// Create a new Function from the given defaults. +// will default to the absolute path of the current working directory. +// will default to the current working directory. +// When is provided but is not, a directory is created +// in the current working directory and used for . func (c *Client) Create(cfg Function) (err error) { + // convert Root path to absolute + cfg.Root, err = filepath.Abs(cfg.Root) + if err != nil { + return + } + // Create project root directory, if it doesn't already exist if err = os.MkdirAll(cfg.Root, 0755); err != nil { return } - // Root must not already be a Function - // - // Instantiate a Function struct about the given root path, but - // immediately exit with error (prior to actual creation) if this is - // a Function already initialized at that path (Create should never - // clobber a pre-existing Function) - f, err := NewFunctionFromDefaults(cfg) + // Create should never clobber a pre-existing Function + hasFunc, err := hasInitializedFunction(cfg.Root) if err != nil { - return + return err } - if f.Initialized() { - err = fmt.Errorf("Function at '%v' already initialized", f.Root) - return + if hasFunc { + return fmt.Errorf("Function at '%v' already initialized", cfg.Root) } - // Root must not contain any visible files - // - // We know from above that the target directory does not contain a Function, - // but also immediately exit if the target directoy contains any visible files - // at all, or any of the known hidden files that will be written. - // This is to ensure that if a user inadvertently chooses an incorrect directory - // for their new Function, the template and config file writing steps do not - // cause data loss. - if err = assertEmptyRoot(f.Root); err != nil { - return + // The path for the new Function should not have any contentious files + // (hidden files OK, unless it's one used by Func) + if err := assertEmptyRoot(cfg.Root); err != nil { + return err } - // Write out the template for a Function - // returns a Function which may be mutated based on the content of - // the template (default Function, builders, buildpacks, etc). + // Path is defaulted to the current working directory + if cfg.Root == "" { + if cfg.Root, err = os.Getwd(); err != nil { + return + } + } + + // Name is defaulted to the directory of the given path. + if cfg.Name == "" { + cfg.Name = nameFromPath(cfg.Root) + } + + // Create a new Function + f := NewFunctionWith(cfg) + + // Write out the new Function's Template files. + // Templates contain values which may result in the Function being mutated + // (default builders, etc), so a new (potentially mutated) Function is + // returned from Templates.Write f, err = c.Templates().Write(f) if err != nil { return } - // Mark it as having been created via this client library and Write (save) + // Mark the Function as having been created f.Created = time.Now() if err = f.Write(); err != nil { return @@ -502,7 +514,7 @@ func (c *Client) Build(ctx context.Context, path string) (err error) { "Don't give up on me", "This is taking a while", "Still building"} - ticker := time.NewTicker(5 * time.Second) + ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() go func() { for { diff --git a/client_test.go b/client_test.go index 310fd9587..311db862c 100644 --- a/client_test.go +++ b/client_test.go @@ -40,11 +40,55 @@ func TestNew(t *testing.T) { client := fn.New(fn.WithRegistry(TestRegistry), fn.WithVerbose(true)) - if err := client.New(context.Background(), fn.Function{Root: root}); err != nil { + if err := client.New(context.Background(), fn.Function{Root: root, Runtime: TestRuntime}); err != nil { t.Fatal(err) } } +// TestRuntimeRequired ensures that the the runtime is an expected value. +func TestRuntimeRequired(t *testing.T) { + // Create a root for the new Function + root := "testdata/example.com/testRuntimeRequired" + defer Using(t, root)() + + client := fn.New(fn.WithRegistry(TestRegistry)) + + // Create a new function at root with all defaults. + err := client.New(context.Background(), fn.Function{Root: root}) + if err == nil { + t.Fatalf("did not receive error creating a function without specifying runtime") + } +} + +// TestNameDefaults ensures that a newly created Function has its name defaulted +// to a name which can be dervied from the last part of the given root path. +func TestNameDefaults(t *testing.T) { + root := "testdata/example.com/testNameDefaults" + defer Using(t, root)() + + client := fn.New(fn.WithRegistry(TestRegistry)) + + f := fn.Function{ + Runtime: TestRuntime, + // NO NAME + Root: root, + } + + if err := client.New(context.Background(), f); err != nil { + t.Fatal(err) + } + + f, err := fn.NewFunction(root) + if err != nil { + t.Fatal(err) + } + + expected := "testNameDefaults" + if f.Name != expected { + t.Fatalf("name was not defaulted. expected '%v' got '%v'", expected, f.Name) + } +} + // TestWritesTemplate ensures the config file and files from the template // are written on new. func TestWritesTemplate(t *testing.T) { @@ -53,7 +97,7 @@ func TestWritesTemplate(t *testing.T) { client := fn.New(fn.WithRegistry(TestRegistry)) - if err := client.New(context.Background(), fn.Function{Root: root}); err != nil { + if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root}); err != nil { t.Fatal(err) } @@ -77,7 +121,7 @@ func TestExtantAborts(t *testing.T) { client := fn.New(fn.WithRegistry(TestRegistry)) // First .New should succeed... - if err := client.New(context.Background(), fn.Function{Root: root}); err != nil { + if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root}); err != nil { t.Fatal(err) } @@ -126,37 +170,11 @@ func TestHiddenFilesIgnored(t *testing.T) { } // Should succeed without error, ignoring the hidden file. - if err := client.New(context.Background(), fn.Function{Root: root}); err != nil { + if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root}); err != nil { t.Fatal(err) } } -// TestDefaultRuntime ensures that the default runtime is applied to new -// Functions and persisted. -func TestDefaultRuntime(t *testing.T) { - // Create a root for the new Function - root := "testdata/example.com/testDefaultRuntime" - defer Using(t, root)() - - client := fn.New(fn.WithRegistry(TestRegistry)) - - // Create a new function at root with all defaults. - if err := client.New(context.Background(), fn.Function{Root: root}); err != nil { - t.Fatal(err) - } - - // Load the function - f, err := fn.NewFunction(root) - if err != nil { - t.Fatal(err) - } - - // Ensure it has defaulted runtime - if f.Runtime != fn.DefaultRuntime { - t.Fatal("The default runtime was not applied or persisted.") - } -} - // TestRepositoriesExtensible ensures that templates are extensible // using a custom path to template repositories on disk. The custom repositories // location is not defined herein but expected to be provided because, for @@ -274,7 +292,7 @@ func TestNamed(t *testing.T) { client := fn.New(fn.WithRegistry(TestRegistry)) - if err := client.New(context.Background(), fn.Function{Root: root, Name: name}); err != nil { + if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root, Name: name}); err != nil { t.Fatal(err) } @@ -321,7 +339,7 @@ func TestDeriveImage(t *testing.T) { // Create the function which calculates fields such as name and image. client := fn.New(fn.WithRegistry(TestRegistry)) - if err := client.New(context.Background(), fn.Function{Root: root}); err != nil { + if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root}); err != nil { t.Fatal(err) } @@ -350,7 +368,7 @@ func TestDeriveImageDefaultRegistry(t *testing.T) { // Rather than use TestRegistry, use a single-token name and expect // the DefaultRegistry to be prepended. client := fn.New(fn.WithRegistry("alice")) - if err := client.New(context.Background(), fn.Function{Root: root}); err != nil { + if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root}); err != nil { t.Fatal(err) } @@ -397,12 +415,8 @@ func TestNewDelegates(t *testing.T) { // The builder should be invoked with a path to a Function project's source // An example image name is returned. builder.BuildFn = func(f fn.Function) error { - expectedPath, err := filepath.Abs(root) - if err != nil { - t.Fatal(err) - } - if expectedPath != f.Root { - t.Fatalf("builder expected path %v, got '%v'", expectedPath, f.Root) + if root != f.Root { + t.Fatalf("builder expected path %v, got '%v'", root, f.Root) } return nil } @@ -429,7 +443,7 @@ func TestNewDelegates(t *testing.T) { // Invoke the creation, triggering the Function delegates, and // perform follow-up assertions that the Functions were indeed invoked. - if err := client.New(context.Background(), fn.Function{Root: root}); err != nil { + if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root}); err != nil { t.Fatal(err) } @@ -454,7 +468,7 @@ func TestRun(t *testing.T) { // Create a client with the mock runner and the new test Function runner := mock.NewRunner() client := fn.New(fn.WithRegistry(TestRegistry), fn.WithRunner(runner)) - if err := client.New(context.Background(), fn.Function{Root: root}); err != nil { + if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root}); err != nil { t.Fatal(err) } @@ -467,12 +481,8 @@ func TestRun(t *testing.T) { if !runner.RunInvoked { t.Fatal("run did not invoke the runner") } - absRoot, err := filepath.Abs(root) - if err != nil { - t.Fatal(err) - } - if runner.RootRequested != absRoot { - t.Fatalf("expected path '%v', got '%v'", absRoot, runner.RootRequested) + if runner.RootRequested != root { + t.Fatalf("expected path '%v', got '%v'", root, runner.RootRequested) } } @@ -499,7 +509,7 @@ func TestUpdate(t *testing.T) { fn.WithDeployer(deployer)) // create the new Function which will be updated - if err := client.New(context.Background(), fn.Function{Root: root}); err != nil { + if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root}); err != nil { t.Fatal(err) } @@ -567,7 +577,7 @@ func TestRemoveByPath(t *testing.T) { fn.WithRegistry(TestRegistry), fn.WithRemover(remover)) - if err := client.New(context.Background(), fn.Function{Root: root}); err != nil { + if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root}); err != nil { t.Fatal(err) } @@ -603,7 +613,7 @@ func TestRemoveByName(t *testing.T) { fn.WithRegistry(TestRegistry), fn.WithRemover(remover)) - if err := client.Create(fn.Function{Root: root}); err != nil { + if err := client.Create(fn.Function{Runtime: TestRuntime, Root: root}); err != nil { t.Fatal(err) } @@ -701,7 +711,7 @@ func TestDeployUnbuilt(t *testing.T) { client := fn.New(fn.WithRegistry(TestRegistry)) // Initialize (half-create) a new Function at root - if err := client.Create(fn.Function{Root: root}); err != nil { + if err := client.Create(fn.Function{Runtime: TestRuntime, Root: root}); err != nil { t.Fatal(err) } @@ -746,29 +756,34 @@ func TestEmit(t *testing.T) { func TestWithConfiguredBuilders(t *testing.T) { root := "testdata/example.com/testConfiguredBuilders" // Root from which to run the test defer Using(t, root)() - - builders := map[string]string{ - "custom": "docker.io/example/custom", - } client := fn.New(fn.WithRegistry(TestRegistry)) - if err := client.Create(fn.Function{ - Root: root, - Builders: builders, - }); err != nil { + + // A Function with predefined builders + f0 := fn.Function{ + Runtime: TestRuntime, + Root: root, + Builders: map[string]string{ + "custom": "docker.io/example/custom", + }} + + // Create the Function, which should preserve custom builders + if err := client.Create(f0); err != nil { t.Fatal(err) } - f, err := fn.NewFunction(root) + + // Load the Function from disk + f1, err := fn.NewFunction(root) if err != nil { t.Fatal(err) } - // Assert that our custom builder array was set - if !reflect.DeepEqual(f.Builders, builders) { - t.Fatalf("Expected %v but got %v", builders, f.Builders) + // Assert that our custom builders were retained + if !reflect.DeepEqual(f0.Builders, f1.Builders) { + t.Fatalf("Expected %v but got %v", f0.Builders, f1.Builders) } - // But that the default still exists - if f.Builder == "" { + // But that the default exists + if f1.Builder == "" { t.Fatal("Expected default builder to be set") } } @@ -786,6 +801,7 @@ func TestWithConfiguredBuildersWithDefault(t *testing.T) { } client := fn.New(fn.WithRegistry(TestRegistry)) if err := client.Create(fn.Function{ + Runtime: TestRuntime, Root: root, Builders: builders, }); err != nil { @@ -818,6 +834,7 @@ func TestWithConfiguredBuildpacks(t *testing.T) { } client := fn.New(fn.WithRegistry(TestRegistry)) if err := client.Create(fn.Function{ + Runtime: TestRuntime, Root: root, Buildpacks: buildpacks, }); err != nil { @@ -889,7 +906,7 @@ func TestCreateStamp(t *testing.T) { client := fn.New(fn.WithRegistry(TestRegistry)) - if err := client.New(context.Background(), fn.Function{Root: root}); err != nil { + if err := client.New(context.Background(), fn.Function{Runtime: TestRuntime, Root: root}); err != nil { t.Fatal(err) } diff --git a/function.go b/function.go index ddccc2170..fdd721795 100644 --- a/function.go +++ b/function.go @@ -17,6 +17,11 @@ import ( const FunctionFile = "func.yaml" type Function struct { + // Version at which this function is known to be compatible. + // More specifically, it is the highest migration which has been applied. + // For details see the .Migrated() and .Migrate() methods. + Version string // semver format + // Root on disk at which to find/create source and config files. Root string `yaml:"-"` @@ -89,35 +94,85 @@ type Function struct { Created time.Time } -// NewFunction loads a Function from a path on disk. -// Errors are returned if the path is not valid, the serialized field could not -// be accessed, or if the contents of the file could not be unmarshaled into a -// Function. A valid path with no associated FunctionFile is not an error but -// rather returns a Function with static defaults set, and will return false -// from .Initialized(). -func NewFunction(root string) (f Function, err error) { - // NewFunction is essentially a convenience/decorator over the more fully- - // featured constructor which takes a full function object as defaults. - return NewFunctionFromDefaults(Function{Root: root}) +// NewFunctionWith defaults as provided. +func NewFunctionWith(defaults Function) Function { + if defaults.Version == "" { + defaults.Version = DefaultVersion + } + if defaults.Template == "" { + defaults.Template = DefaultTemplate + } + return defaults } -// NewFunctionFromDefaults is equivalent to calling NewFunction, but will use -// the provided function as defaults. -func NewFunctionFromDefaults(f Function) (Function, error) { - var err error - if f.Runtime == "" { - f.Runtime = DefaultRuntime +// NewFunction from a given path. +// Invalid paths, or no Function at path are errors. +// Syntactic errors are returned immediately (yaml unmarshal errors). +// Functions which are syntactically valid are also then logically validated. +// Functions from earlier versions are brought up to current using migrations. +// Migrations are run prior to validators such that validation can omit +// concerning itself with backwards compatibility. Migrators must therefore +// selectively consider the minimal validation necesssary to ehable migration. +func NewFunction(path string) (f Function, err error) { + f.Root = path // path is not persisted, as this is the purvew of the FS itself + var filename = filepath.Join(path, FunctionFile) + if _, err = os.Stat(filename); err != nil { + return } - if f.Template == "" { - f.Template = DefaultTemplate + bb, err := ioutil.ReadFile(filename) + if err != nil { + return } - if f.Root, err = filepath.Abs(f.Root); err != nil { - return f, err + if err = yaml.UnmarshalStrict(bb, &f); err != nil { + err = formatUnmarshalError(err) // human-friendly unmarshalling errors + return } + if f, err = f.Migrate(); err != nil { + return + } + return f, f.Validate() +} + +// Validate Function is logically correct, returning a bundled, and quite +// verbose, formatted error detailing any issues. +func (f Function) Validate() error { if f.Name == "" { - f.Name = nameFromPath(f.Root) + return errors.New("function name is required") } - return unmarshalFunction(f) + if f.Runtime == "" { + return errors.New("function language runtime is required") + } + if f.Root == "" { + return errors.New("function root path is required") + } + + var ctr int + errs := [][]string{ + validateVolumes(f.Volumes), + ValidateBuildEnvs(f.BuildEnvs), + ValidateEnvs(f.Envs), + validateOptions(f.Options), + ValidateLabels(f.Labels), + } + + var b strings.Builder + b.WriteString(fmt.Sprintf("'%v' contains errors:", FunctionFile)) + + for _, ee := range errs { + if len(ee) > 0 { + b.WriteString("\n") // Precede each group of errors with a linebreak + } + for _, e := range ee { + ctr++ + b.WriteString("\t" + e) + } + } + + if ctr == 0 { + return nil // Return nil if there were no validation errors. + } + + return errors.New(b.String()) } // nameFromPath returns the default name for a Function derived from a path. @@ -282,7 +337,7 @@ var contentiousFiles = []string{ FunctionFile, } -// contentiousFilesIn the given directoy +// contentiousFilesIn the given directory func contentiousFilesIn(dir string) (contentious []string, err error) { files, err := ioutil.ReadDir(dir) for _, file := range files { @@ -310,66 +365,29 @@ func isEffectivelyEmpty(dir string) (bool, error) { return true, nil } -// unmarshalFunction from disk (FunctionFile) using the passed Function as -// its defaults. If no serialized function exists at path, the Function -// returned is equivalent to the default passed. -func unmarshalFunction(f Function) (Function, error) { +// returns true if the given path contains an initialized Function. +func hasInitializedFunction(path string) (bool, error) { var err error - var filename = filepath.Join(f.Root, FunctionFile) + var filename = filepath.Join(path, FunctionFile) - // Return if there is no file to load, or if there is an error reading. if _, err = os.Stat(filename); err != nil { if os.IsNotExist(err) { - err = nil // missing file is not an error. + return false, nil } - return f, err + return false, err // invalid path or access error } - - // Load the file bb, err := ioutil.ReadFile(filename) if err != nil { - return f, err + return false, err } - - // Unmarshal as yaml + f := Function{} if err = yaml.UnmarshalStrict(bb, &f); err != nil { - // Return immediately if there are syntactic errors. - return f, formatUnmarshalError(err) + return false, err } - - return f, validateFunction(f) -} - -// Validate Function is logically correct, returning a bundled, and quite -// verbose, formatted error detailing any issues. -func validateFunction(f Function) error { - var ctr int - errs := [][]string{ - validateVolumes(f.Volumes), - ValidateBuildEnvs(f.BuildEnvs), - ValidateEnvs(f.Envs), - validateOptions(f.Options), - ValidateLabels(f.Labels), + if f, err = f.Migrate(); err != nil { + return false, err } - - var b strings.Builder - b.WriteString(fmt.Sprintf("'%v' contains errors:", FunctionFile)) - - for _, ee := range errs { - if len(ee) > 0 { - b.WriteString("\n") // Precede each group of errors with a linebreak - } - for _, e := range ee { - ctr++ - b.WriteString("\t" + e) - } - } - - if ctr == 0 { - return nil // Return nil if there were no validation errors. - } - - return errors.New(b.String()) + return f.Initialized(), nil } // Format yaml unmarshall error to be more human friendly. diff --git a/function_migrations.go b/function_migrations.go new file mode 100644 index 000000000..4863a5887 --- /dev/null +++ b/function_migrations.go @@ -0,0 +1,114 @@ +package function + +import ( + "time" + + "github.com/coreos/go-semver/semver" +) + +// Migrate applies any necessary migrations, returning a new migrated +// version of the Function. It is the caller's responsibility to +// .Write() the Function to persist to disk. +func (f Function) Migrate() (migrated Function, err error) { + // Return immediately if the Function indicates it has already been + // migrated. + if f.Migrated() { + return f, nil + } + + // If the version is empty, treat it as 0.0.0 + if f.Version == "" { + f.Version = DefaultVersion + } + + migrated = f // initially equivalent + for _, m := range migrations { + // Skip this migration if the current function's version is not less than + // the migration's applicable verion. + if !semver.New(migrated.Version).LessThan(*semver.New(m.version)) { + continue + } + // Apply this migration when the Function's version is less than that which + // the migration will impart. + migrated, err = m.migrate(migrated, m) + if err != nil { + return // fail fast on any migration errors + } + } + return +} + +// migration is a migration which should be applied to Functions whose version +// is below that indicated. +type migration struct { + version string // version before which this migration may be needed. + migrate migrator // Migrator migrates. +} + +// migrator is a function which returns a migrated copy of an inbound function. +type migrator func(Function, migration) (Function, error) + +// Migrated returns whether or not the Function has been migrated to the highest +// level the currently executing system is aware of (or beyond). +// returns true. +func (f Function) Migrated() bool { + // If the function has no Version, it is pre-migrations and is implicitly + // not migrated. + if f.Version == "" { + return false + } + + // lastMigration is the last registered migration. + lastMigration := semver.New(migrations[len(migrations)-1].version) + + // Fail the migration test if the Function's version is less than + // the latest available. + return !semver.New(f.Version).LessThan(*lastMigration) +} + +// Migrations registry +// ------------------- + +// migrations are all migrators in ascending order. +// No two migrations may have the exact version number (introduce a patch +// version for the migration if necessary) +var migrations = []migration{ + {"0.19.0", migrateToCreationStamp}, + // New Migrations Here. +} + +// Individual Migration implementations +// ------------------------------------ + +// migrateToCreationStamp is the initial migration which brings a Function from +// some unknown point in the past to the point at which it is versioned, +// migrated and includes a creation timestamp. Without this migration, +// instantiation of old functions will fail with a "Function at path X not +// initialized" in Func versions above v0.19.0 +// +// This migration must be aware of the difference between a Function which +// was previously created (but with no create stamp), and a Function which +// exists only in memory and should legitimately fail the .Initialized() check. +// The only way to know is to check a side-effect of earlier versions: +// are the .Name and .Runtime fields populated. This was the way the +// .Initialized check was implemented prior to versioning being introduced, so +// it is equivalent logically to use this here as well. + +// In summary: if the creation stamp is zero, but name and runtime fields are +// populated, then this is an old Function and should be migrated to having a +// create stamp. Otherwise, this is an in-memory (new) Function that is +// currently in the process of being created and as such need not be mutated +// to consider this migration having been evaluated. +func migrateToCreationStamp(f Function, m migration) (Function, error) { + // For functions with no creation timestamp, but appear to have been pre- + // existing, populate their create stamp and version. + // Yes, it's a little gnarly, but bootstrapping into the lovelieness of a + // versioned/migrated system takes cleaning up the trash. + if f.Created.IsZero() { // If there is no create stamp + if f.Name != "" && f.Runtime != "" { // and it appears to be an old Function + f.Created = time.Now() // Migrate it to having a timestamp. + } + } + f.Version = m.version // Record this migration was evaluated. + return f, nil +} diff --git a/function_migrations_unit_test.go b/function_migrations_unit_test.go new file mode 100644 index 000000000..18fb92301 --- /dev/null +++ b/function_migrations_unit_test.go @@ -0,0 +1,84 @@ +package function + +import ( + "testing" + "time" + + "github.com/coreos/go-semver/semver" +) + +// TestMigrated ensures that the .Migrated() method returns whether or not the +// migrations were applied based on its self-reported .Version member. +func TestMigrated(t *testing.T) { + // A Function with no migration stamp + f := Function{} + if f.Migrated() { + t.Fatal("function with no version stamp should be not migrated.") + } + + // A Function with a migration stamp that is explicitly less than the + // latest known. + f = Function{Version: "0.0.1"} + if f.Migrated() { + t.Fatalf("function with version %v when latest is %v should be !migrated", + f.Version, latestMigrationVersion()) + } + + // A Function with a version stamp equivalent to the latest is up-to-date + // and should be considered migrated. + f = Function{Version: latestMigrationVersion()} + if !f.Migrated() { + t.Fatalf("function version %v should me considered migrated (latest is %v)", + f.Version, latestMigrationVersion()) + } + + // A Function with a version stamp beyond what is recognized by the current + // codebase is considered fully migrated, for purposes of this version of func + v0 := semver.New(latestMigrationVersion()) + v0.BumpMajor() + f = Function{Version: v0.String()} + if !f.Migrated() { + t.Fatalf("Function with version %v should be considered migrated when latest known by this codebase is %v", f.Version, latestMigrationVersion()) + } +} + +// TestMigrate ensures that Functions have migrations apply the version +// stamp on instantiation indicating migrations have been applied. +func TestMigrate(t *testing.T) { + // Load an old Function, as it an earlier version it has registered migrations + // that will need to be applied. + root := "testdata/migrations/v0.19.0" + + // Instantiate the Function with the antiquated structure, which should cause + // migrations to be applied in order, and result in a function whose version + // compatibility is equivalent to the latest registered migration. + f, err := NewFunction(root) + if err != nil { + t.Fatal(err) + } + if f.Version != latestMigrationVersion() { + t.Fatalf("Function was not migrated to %v on instantiation: version is %v", + latestMigrationVersion(), f.Version) + } +} + +// TestMigrateToCreationStamp ensures that the creation timestamp migration +// introduced for functions 0.19.0 and earlier is applied. +func TestMigrateToCreationStamp(t *testing.T) { + // Load a Function of version 0.19.0, which should have the migration applied + root := "testdata/migrations/v0.19.0" + + now := time.Now() + f, err := NewFunction(root) + if err != nil { + t.Fatal(err) + } + + if f.Created.Before(now) { + t.Fatalf("migration not applied: expected timestamp to be now, got %v.", f.Created) + } +} + +func latestMigrationVersion() string { + return migrations[len(migrations)-1].version +} diff --git a/function_test.go b/function_test.go index 391889cda..a3e51c6c3 100644 --- a/function_test.go +++ b/function_test.go @@ -4,21 +4,79 @@ package function_test import ( + "reflect" "testing" fn "knative.dev/kn-plugin-func" . "knative.dev/kn-plugin-func/testing" ) - -// TestFunctionNameDefault ensures that a Function's name is defaulted to that -// which can be derived from the last part of its path. -func TestFunctionNameDefault(t *testing.T) { - root := "testdata/testFunctionNameDefault" - defer Using(t, root)() - _, err := fn.NewFunction(root) + +// TestWriteIdempotency ensures that a Function can be written repeatedly +// without change. +func TestWriteIdempotency(t *testing.T) { + root, rm := Mktemp(t) + defer rm() + client := fn.New(fn.WithRegistry(TestRegistry)) + + // Create a function + f := fn.Function{ + Runtime: TestRuntime, + Root: root, + } + if err := client.Create(f); err != nil { + t.Fatal(err) + } + + // Load the function and write it again + f1, err := fn.NewFunction(root) if err != nil { t.Fatal(err) } - // TODO - // Test that the name was defaulted -} \ No newline at end of file + if err := f1.Write(); err != nil { + t.Fatal(err) + } + + // Load it again and compare + f2, err := fn.NewFunction(root) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(f1, f2) { + t.Fatalf("function differs after reload.") + } +} + +// TestFunctionNameDefault ensures that a Function's name is defaulted to that +// which can be derived from the last part of its path. +// Creating a new Function from a path will error if there is no Function at +// that path. Creating using the client initializes the default. +func TestFunctionNameDefault(t *testing.T) { + // A path at which there is no Function currently + root := "testdata/testFunctionNameDefault" + defer Using(t, root)() + f, err := fn.NewFunction(root) + if err == nil { + t.Fatal("expected error instantiating a nonexistant Function") + } + + // Create the Function at the path + client := fn.New(fn.WithRegistry(TestRegistry)) + f = fn.Function{ + Runtime: TestRuntime, + Root: root, + } + if err := client.Create(f); err != nil { + t.Fatal(err) + } + + // Load the (now extant) Function + f, err = fn.NewFunction(root) + if err != nil { + t.Fatal(err) + } + + // Verify the name was defaulted as expected + if f.Name != "testFunctionNameDefault" { + t.Fatalf("expected name 'testFunctionNameDefault', got '%v'", f.Name) + } +} diff --git a/function_unit_test.go b/function_unit_test.go index 41d99c981..faec977f0 100644 --- a/function_unit_test.go +++ b/function_unit_test.go @@ -119,20 +119,14 @@ func Test_DerivedImage(t *testing.T) { root := "testdata/" + tt.fnName defer Using(t, root)() - f := Function{ - Name: tt.fnName, - Root: root, - Image: tt.image, - } - // write out the function client := New() - err := client.Create(f) + err := client.Create(Function{Runtime: "go", Name: tt.fnName, Root: root}) if err != nil { t.Fatal(err) } - got, err := DerivedImage(f.Root, tt.registry) + got, err := DerivedImage(root, tt.registry) if err != nil { t.Errorf("DerivedImage() for image %v and registry %v; got error %v", tt.image, tt.registry, err) } diff --git a/go.mod b/go.mod index a999454eb..9bb36b0b6 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/buildpacks/pack v0.22.0 github.com/cloudevents/sdk-go/v2 v2.5.0 github.com/containers/image/v5 v5.10.6 + github.com/coreos/go-semver v0.3.0 github.com/docker/cli v20.10.10+incompatible github.com/docker/docker v20.10.10+incompatible github.com/docker/docker-credential-helpers v0.6.4 diff --git a/go.sum b/go.sum index cbe9be588..e534543e9 100644 --- a/go.sum +++ b/go.sum @@ -334,6 +334,7 @@ github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmeka github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= diff --git a/repository.go b/repository.go index 3939c28b6..92b75acb7 100644 --- a/repository.go +++ b/repository.go @@ -203,7 +203,7 @@ func filesystemFromPath(uri string) (f Filesystem, err error) { return osFilesystem{root: parsed.Path}, nil } -// repositoryRuntimes returns runtimes defined in this repository's filesytem. +// repositoryRuntimes returns runtimes defined in this repository's filesystem. // The views are denormalized, using the parent repository's values // for inherited fields BuildConfig and HealthEndpoints as the default values // for the runtimes and templates. The runtimes and templates themselves can @@ -429,7 +429,7 @@ func (r *Repository) Runtime(name string) (runtime Runtime, err error) { func (r *Repository) Write(path string) error { // NOTE: Writing internal .git directory does not work // - // A quirk of the git library's implementation is that the filesytem + // A quirk of the git library's implementation is that the filesystem // returned does not include the .git directory. This is usually not an // issue when utilizing the repository's filesystem (for writing templates), // but it does cause problems here (used for installing a repo locally) where diff --git a/schema/func_yaml-schema.json b/schema/func_yaml-schema.json index 314267467..292b5fb20 100644 --- a/schema/func_yaml-schema.json +++ b/schema/func_yaml-schema.json @@ -20,6 +20,7 @@ }, "Function": { "required": [ + "Version", "name", "namespace", "runtime", @@ -39,6 +40,9 @@ "Created" ], "properties": { + "Version": { + "type": "string" + }, "name": { "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", "type": "string" diff --git a/templates.go b/templates.go index 37d4f2eab..97c9043a8 100644 --- a/templates.go +++ b/templates.go @@ -96,11 +96,17 @@ func (t *Templates) Get(runtime, fullname string) (Template, error) { return repo.Template(runtime, tplName) } -// Write a template to disk for the given Function +// Write a function's template to disk. // Returns a Function which may have been modified dependent on the content // of the template (which can define default Function fields, builders, // buildpacks, etc) func (t *Templates) Write(f Function) (Function, error) { + // Templates require an initially valid Function to write + // (has name, path, runtime etc) + if err := f.Validate(); err != nil { + return f, err + } + // The Function's Template template, err := t.Get(f.Runtime, f.Template) if err != nil { diff --git a/testdata/migrations/README.md b/testdata/migrations/README.md new file mode 100644 index 000000000..75bd3f402 --- /dev/null +++ b/testdata/migrations/README.md @@ -0,0 +1,4 @@ +# Migrations + +Contains Functions created with earlier versions to ensure backwards +compatibility and test migrations where applicable. diff --git a/testdata/migrations/v0.19.0/func.yaml b/testdata/migrations/v0.19.0/func.yaml new file mode 100644 index 000000000..eeaa66cda --- /dev/null +++ b/testdata/migrations/v0.19.0/func.yaml @@ -0,0 +1,22 @@ +name: testfunc +namespace: "" +runtime: go +image: "" +imageDigest: "" +builder: gcr.io/paketo-buildpacks/builder:base +builders: + base: gcr.io/paketo-buildpacks/builder:base + default: gcr.io/paketo-buildpacks/builder:base + full: gcr.io/paketo-buildpacks/builder:full +buildpacks: +- paketo-buildpacks/go-dist +- ghcr.io/boson-project/go-function-buildpack:tip +healthEndpoints: + liveness: /health/liveness + readiness: /health/readiness +volumes: [] +buildEnvs: [] +envs: [] +annotations: {} +options: {} +labels: [] diff --git a/testing/testing.go b/testing/testing.go index 8b9ed9ccf..c23eb657b 100644 --- a/testing/testing.go +++ b/testing/testing.go @@ -25,7 +25,6 @@ import ( "testing" ) -// USING: Make specified dir. Return deferrable cleanup fn. // Using the given path, create it as a new directory and return a deferrable // which will remove it. // usage: @@ -73,7 +72,7 @@ func Within(t *testing.T, root string) func() { // Mktemp creates a temporary directory, CDs the current processes (test) to // said directory, and returns the path to said directory. // Usage: -// path, rm := mktemp(t) +// path, rm := Mktemp(t) // defer rm() // CWD is now 'path' // errors encountererd fail the current test. @@ -148,6 +147,9 @@ func WithEnvVar(t *testing.T, name, value string) func() { } } +// WithExecutable creates an executable of the given name and source in a temp +// directory which is then added to PATH. Returned is a deferrable which will +// clean up both the script and PATH. func WithExecutable(t *testing.T, name, goSrc string) func() { var err error binDir := t.TempDir() diff --git a/third_party/VENDOR-LICENSE/github.com/coreos/go-semver/semver/LICENSE b/third_party/VENDOR-LICENSE/github.com/coreos/go-semver/semver/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/third_party/VENDOR-LICENSE/github.com/coreos/go-semver/semver/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/third_party/VENDOR-LICENSE/github.com/coreos/go-semver/semver/NOTICE b/third_party/VENDOR-LICENSE/github.com/coreos/go-semver/semver/NOTICE new file mode 100644 index 000000000..23a0ada2f --- /dev/null +++ b/third_party/VENDOR-LICENSE/github.com/coreos/go-semver/semver/NOTICE @@ -0,0 +1,5 @@ +CoreOS Project +Copyright 2018 CoreOS, Inc + +This product includes software developed at CoreOS, Inc. +(http://www.coreos.com/). diff --git a/vendor/github.com/coreos/go-semver/LICENSE b/vendor/github.com/coreos/go-semver/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/vendor/github.com/coreos/go-semver/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/vendor/github.com/coreos/go-semver/NOTICE b/vendor/github.com/coreos/go-semver/NOTICE new file mode 100644 index 000000000..23a0ada2f --- /dev/null +++ b/vendor/github.com/coreos/go-semver/NOTICE @@ -0,0 +1,5 @@ +CoreOS Project +Copyright 2018 CoreOS, Inc + +This product includes software developed at CoreOS, Inc. +(http://www.coreos.com/). diff --git a/vendor/github.com/coreos/go-semver/semver/semver.go b/vendor/github.com/coreos/go-semver/semver/semver.go new file mode 100644 index 000000000..76cf4852c --- /dev/null +++ b/vendor/github.com/coreos/go-semver/semver/semver.go @@ -0,0 +1,296 @@ +// Copyright 2013-2015 CoreOS, Inc. +// +// 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. + +// Semantic Versions http://semver.org +package semver + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + +type Version struct { + Major int64 + Minor int64 + Patch int64 + PreRelease PreRelease + Metadata string +} + +type PreRelease string + +func splitOff(input *string, delim string) (val string) { + parts := strings.SplitN(*input, delim, 2) + + if len(parts) == 2 { + *input = parts[0] + val = parts[1] + } + + return val +} + +func New(version string) *Version { + return Must(NewVersion(version)) +} + +func NewVersion(version string) (*Version, error) { + v := Version{} + + if err := v.Set(version); err != nil { + return nil, err + } + + return &v, nil +} + +// Must is a helper for wrapping NewVersion and will panic if err is not nil. +func Must(v *Version, err error) *Version { + if err != nil { + panic(err) + } + return v +} + +// Set parses and updates v from the given version string. Implements flag.Value +func (v *Version) Set(version string) error { + metadata := splitOff(&version, "+") + preRelease := PreRelease(splitOff(&version, "-")) + dotParts := strings.SplitN(version, ".", 3) + + if len(dotParts) != 3 { + return fmt.Errorf("%s is not in dotted-tri format", version) + } + + if err := validateIdentifier(string(preRelease)); err != nil { + return fmt.Errorf("failed to validate pre-release: %v", err) + } + + if err := validateIdentifier(metadata); err != nil { + return fmt.Errorf("failed to validate metadata: %v", err) + } + + parsed := make([]int64, 3, 3) + + for i, v := range dotParts[:3] { + val, err := strconv.ParseInt(v, 10, 64) + parsed[i] = val + if err != nil { + return err + } + } + + v.Metadata = metadata + v.PreRelease = preRelease + v.Major = parsed[0] + v.Minor = parsed[1] + v.Patch = parsed[2] + return nil +} + +func (v Version) String() string { + var buffer bytes.Buffer + + fmt.Fprintf(&buffer, "%d.%d.%d", v.Major, v.Minor, v.Patch) + + if v.PreRelease != "" { + fmt.Fprintf(&buffer, "-%s", v.PreRelease) + } + + if v.Metadata != "" { + fmt.Fprintf(&buffer, "+%s", v.Metadata) + } + + return buffer.String() +} + +func (v *Version) UnmarshalYAML(unmarshal func(interface{}) error) error { + var data string + if err := unmarshal(&data); err != nil { + return err + } + return v.Set(data) +} + +func (v Version) MarshalJSON() ([]byte, error) { + return []byte(`"` + v.String() + `"`), nil +} + +func (v *Version) UnmarshalJSON(data []byte) error { + l := len(data) + if l == 0 || string(data) == `""` { + return nil + } + if l < 2 || data[0] != '"' || data[l-1] != '"' { + return errors.New("invalid semver string") + } + return v.Set(string(data[1 : l-1])) +} + +// Compare tests if v is less than, equal to, or greater than versionB, +// returning -1, 0, or +1 respectively. +func (v Version) Compare(versionB Version) int { + if cmp := recursiveCompare(v.Slice(), versionB.Slice()); cmp != 0 { + return cmp + } + return preReleaseCompare(v, versionB) +} + +// Equal tests if v is equal to versionB. +func (v Version) Equal(versionB Version) bool { + return v.Compare(versionB) == 0 +} + +// LessThan tests if v is less than versionB. +func (v Version) LessThan(versionB Version) bool { + return v.Compare(versionB) < 0 +} + +// Slice converts the comparable parts of the semver into a slice of integers. +func (v Version) Slice() []int64 { + return []int64{v.Major, v.Minor, v.Patch} +} + +func (p PreRelease) Slice() []string { + preRelease := string(p) + return strings.Split(preRelease, ".") +} + +func preReleaseCompare(versionA Version, versionB Version) int { + a := versionA.PreRelease + b := versionB.PreRelease + + /* Handle the case where if two versions are otherwise equal it is the + * one without a PreRelease that is greater */ + if len(a) == 0 && (len(b) > 0) { + return 1 + } else if len(b) == 0 && (len(a) > 0) { + return -1 + } + + // If there is a prerelease, check and compare each part. + return recursivePreReleaseCompare(a.Slice(), b.Slice()) +} + +func recursiveCompare(versionA []int64, versionB []int64) int { + if len(versionA) == 0 { + return 0 + } + + a := versionA[0] + b := versionB[0] + + if a > b { + return 1 + } else if a < b { + return -1 + } + + return recursiveCompare(versionA[1:], versionB[1:]) +} + +func recursivePreReleaseCompare(versionA []string, versionB []string) int { + // A larger set of pre-release fields has a higher precedence than a smaller set, + // if all of the preceding identifiers are equal. + if len(versionA) == 0 { + if len(versionB) > 0 { + return -1 + } + return 0 + } else if len(versionB) == 0 { + // We're longer than versionB so return 1. + return 1 + } + + a := versionA[0] + b := versionB[0] + + aInt := false + bInt := false + + aI, err := strconv.Atoi(versionA[0]) + if err == nil { + aInt = true + } + + bI, err := strconv.Atoi(versionB[0]) + if err == nil { + bInt = true + } + + // Numeric identifiers always have lower precedence than non-numeric identifiers. + if aInt && !bInt { + return -1 + } else if !aInt && bInt { + return 1 + } + + // Handle Integer Comparison + if aInt && bInt { + if aI > bI { + return 1 + } else if aI < bI { + return -1 + } + } + + // Handle String Comparison + if a > b { + return 1 + } else if a < b { + return -1 + } + + return recursivePreReleaseCompare(versionA[1:], versionB[1:]) +} + +// BumpMajor increments the Major field by 1 and resets all other fields to their default values +func (v *Version) BumpMajor() { + v.Major += 1 + v.Minor = 0 + v.Patch = 0 + v.PreRelease = PreRelease("") + v.Metadata = "" +} + +// BumpMinor increments the Minor field by 1 and resets all other fields to their default values +func (v *Version) BumpMinor() { + v.Minor += 1 + v.Patch = 0 + v.PreRelease = PreRelease("") + v.Metadata = "" +} + +// BumpPatch increments the Patch field by 1 and resets all other fields to their default values +func (v *Version) BumpPatch() { + v.Patch += 1 + v.PreRelease = PreRelease("") + v.Metadata = "" +} + +// validateIdentifier makes sure the provided identifier satisfies semver spec +func validateIdentifier(id string) error { + if id != "" && !reIdentifier.MatchString(id) { + return fmt.Errorf("%s is not a valid semver identifier", id) + } + return nil +} + +// reIdentifier is a regular expression used to check that pre-release and metadata +// identifiers satisfy the spec requirements +var reIdentifier = regexp.MustCompile(`^[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$`) diff --git a/vendor/github.com/coreos/go-semver/semver/sort.go b/vendor/github.com/coreos/go-semver/semver/sort.go new file mode 100644 index 000000000..e256b41a5 --- /dev/null +++ b/vendor/github.com/coreos/go-semver/semver/sort.go @@ -0,0 +1,38 @@ +// Copyright 2013-2015 CoreOS, Inc. +// +// 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 semver + +import ( + "sort" +) + +type Versions []*Version + +func (s Versions) Len() int { + return len(s) +} + +func (s Versions) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s Versions) Less(i, j int) bool { + return s[i].LessThan(*s[j]) +} + +// Sort sorts the given slice of Version +func Sort(versions []*Version) { + sort.Sort(Versions(versions)) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index af64fa051..823c1611d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -170,6 +170,9 @@ github.com/containers/storage/pkg/mount github.com/containers/storage/pkg/reexec github.com/containers/storage/pkg/system github.com/containers/storage/pkg/unshare +# github.com/coreos/go-semver v0.3.0 +## explicit +github.com/coreos/go-semver/semver # github.com/creack/pty v1.1.11 github.com/creack/pty # github.com/davecgh/go-spew v1.1.1