feat: function version migrations (#664)

* feat: function version migrations

* unmarshall functin now part of initialization

* regenerate schema

* spelling errors
This commit is contained in:
Luke Kingland 2021-11-24 21:50:27 +09:00 committed by GitHub
parent 0eb3fef080
commit ccf00152be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1283 additions and 195 deletions

View File

@ -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.
// <path> will default to the absolute path of the current working directory.
// <name> will default to the current working directory.
// When <name> is provided but <path> is not, a directory <name> is created
// in the current working directory and used for <path>.
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 {

View File

@ -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)
}

View File

@ -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.

114
function_migrations.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
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)
}
}

View File

@ -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)
}

1
go.mod
View File

@ -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

1
go.sum
View File

@ -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=

View File

@ -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

View File

@ -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"

View File

@ -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 {

4
testdata/migrations/README.md vendored Normal file
View File

@ -0,0 +1,4 @@
# Migrations
Contains Functions created with earlier versions to ensure backwards
compatibility and test migrations where applicable.

22
testdata/migrations/v0.19.0/func.yaml vendored Normal file
View File

@ -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: []

View File

@ -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()

View File

@ -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.

View File

@ -0,0 +1,5 @@
CoreOS Project
Copyright 2018 CoreOS, Inc
This product includes software developed at CoreOS, Inc.
(http://www.coreos.com/).

202
vendor/github.com/coreos/go-semver/LICENSE generated vendored Normal file
View File

@ -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.

5
vendor/github.com/coreos/go-semver/NOTICE generated vendored Normal file
View File

@ -0,0 +1,5 @@
CoreOS Project
Copyright 2018 CoreOS, Inc
This product includes software developed at CoreOS, Inc.
(http://www.coreos.com/).

296
vendor/github.com/coreos/go-semver/semver/semver.go generated vendored Normal file
View File

@ -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-]+)*$`)

38
vendor/github.com/coreos/go-semver/semver/sort.go generated vendored Normal file
View File

@ -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))
}

3
vendor/modules.txt vendored
View File

@ -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