schema.go 13.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// Copyright 2017 The kubecfg authors
//
//
//    Licensed under the Apache License, Version 2.0 (the "License");
//    you may not use this file except in compliance with the License.
//    You may obtain a copy of the License at
//
//      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 app

import (
19
	"encoding/json"
20
	"fmt"
21
	"path/filepath"
22

23
	"github.com/blang/semver"
24
	"github.com/ghodss/yaml"
25
	"github.com/pkg/errors"
26
	log "github.com/sirupsen/logrus"
27
	"github.com/spf13/afero"
28 29 30
)

const (
31
	// DefaultAPIVersion is the default ks API version to use if not specified.
32
	DefaultAPIVersion = "0.1.0"
33 34 35 36
	// Kind is the schema resource type.
	Kind = "ksonnet.io/app"
	// DefaultVersion is the default version of the app schema.
	DefaultVersion = "0.0.1"
37 38
)

Jessica Yuen's avatar
Jessica Yuen committed
39
var (
40
	// ErrRegistryNameInvalid is the error where a registry name is invalid.
Jessica Yuen's avatar
Jessica Yuen committed
41
	ErrRegistryNameInvalid = fmt.Errorf("Registry name is invalid")
42 43
	// ErrRegistryExists is the error when trying to create a registry that already exists.
	ErrRegistryExists = fmt.Errorf("Registry with name already exists")
Jessica Yuen's avatar
Jessica Yuen committed
44 45 46 47
	// ErrEnvironmentNameInvalid is the error where an environment name is invalid.
	ErrEnvironmentNameInvalid = fmt.Errorf("Environment name is invalid")
	// ErrEnvironmentExists is the error when trying to create an environment that already exists.
	ErrEnvironmentExists = fmt.Errorf("Environment with name already exists")
48 49
	// ErrEnvironmentNotExists is the error when trying to update an environment that doesn't exist.
	ErrEnvironmentNotExists = fmt.Errorf("Environment with name doesn't exist")
Jessica Yuen's avatar
Jessica Yuen committed
50
)
51

52 53
// Spec defines all the ksonnet project metadata. This includes details such as
// the project name, authors, environments, and registries.
54
type Spec struct {
55 56 57 58 59 60 61 62 63 64 65 66 67 68
	APIVersion   string             `json:"apiVersion,omitempty"`
	Kind         string             `json:"kind,omitempty"`
	Name         string             `json:"name,omitempty"`
	Version      string             `json:"version,omitempty"`
	Description  string             `json:"description,omitempty"`
	Authors      []string           `json:"authors,omitempty"`
	Contributors ContributorSpecs   `json:"contributors,omitempty"`
	Repository   *RepositorySpec    `json:"repository,omitempty"`
	Bugs         string             `json:"bugs,omitempty"`
	Keywords     []string           `json:"keywords,omitempty"`
	Registries   RegistryConfigs    `json:"registries,omitempty"`
	Environments EnvironmentConfigs `json:"environments,omitempty"`
	Libraries    LibraryConfigs     `json:"libraries,omitempty"`
	License      string             `json:"license,omitempty"`
69 70
}

71 72
// Read will return the specification for a ksonnet application. It will navigate up directories
// to search for `app.yaml` and return error if it hits the root directory.
73 74
func read(fs afero.Fs, root string) (*Spec, error) {
	log.Debugf("loading application configuration from %s", root)
75

76
	appConfig, err := afero.ReadFile(fs, specPath(root))
77 78 79 80
	if err != nil {
		return nil, err
	}

81 82 83
	var spec Spec

	err = yaml.Unmarshal(appConfig, &spec)
84 85 86 87
	if err != nil {
		return nil, err
	}

88 89
	if err = spec.validate(); err != nil {
		return nil, err
90 91
	}

92 93 94
	exists, err := afero.Exists(fs, overridePath(root))
	if err != nil {
		return nil, err
95 96
	}

97 98 99 100 101 102 103 104 105 106 107 108 109
	if exists {
		var o Override

		overrideConfig, err := afero.ReadFile(fs, overridePath(root))
		if err != nil {
			return nil, err
		}

		err = yaml.Unmarshal(overrideConfig, &o)
		if err != nil {
			return nil, err
		}

bryanl's avatar
bryanl committed
110 111 112 113 114
		for k, v := range o.Environments {
			v.isOverride = true
			spec.Environments[k] = v
		}

115 116 117 118
		for k, v := range o.Registries {
			v.isOverride = true
			spec.Registries[k] = v
		}
119 120
	}

121 122
	if err := spec.validate(); err != nil {
		return nil, err
123 124
	}

125
	return &spec, nil
126 127
}

128 129 130
// Write writes the provided spec to file system.
func write(fs afero.Fs, appRoot string, spec *Spec) error {
	o := Override{
131 132
		Kind:         overrideKind,
		APIVersion:   overrideVersion,
133
		Environments: EnvironmentConfigs{},
134
		Registries:   RegistryConfigs{},
135
	}
136

137
	overrideKeys := map[string][]string{
bryanl's avatar
bryanl committed
138 139 140 141 142 143 144 145 146
		"environments": make([]string, 0),
		"registries":   make([]string, 0),
	}

	for k, v := range spec.Environments {
		if v.IsOverride() {
			o.Environments[k] = v
			overrideKeys["environments"] = append(overrideKeys["environments"], k)
		}
147 148 149 150 151 152 153 154 155
	}

	for k, v := range spec.Registries {
		if v.IsOverride() {
			o.Registries[k] = v
			overrideKeys["registries"] = append(overrideKeys["registries"], k)
		}
	}

bryanl's avatar
bryanl committed
156 157 158 159
	for _, k := range overrideKeys["environments"] {
		delete(spec.Environments, k)
	}

160 161 162 163 164 165 166 167 168 169 170 171 172
	for _, k := range overrideKeys["registries"] {
		delete(spec.Registries, k)
	}

	appConfig, err := yaml.Marshal(&spec)
	if err != nil {
		return errors.Wrap(err, "convert app configuration to YAML")
	}

	if err = afero.WriteFile(fs, specPath(appRoot), appConfig, DefaultFilePermissions); err != nil {
		return errors.Wrap(err, "write app.yaml")
	}

173
	if err = removeOverride(fs, appRoot); err != nil {
174 175 176
		return errors.Wrap(err, "clean overrides")
	}

177 178
	if o.IsDefined() {
		return SaveOverride(defaultYAMLEncoder, fs, appRoot, &o)
179 180 181 182 183
	}

	return nil
}

184
func removeOverride(fs afero.Fs, appRoot string) error {
185
	exists, err := afero.Exists(fs, overridePath(appRoot))
186 187 188 189
	if err != nil {
		return err
	}

190 191 192 193 194
	if exists {
		return fs.Remove(overridePath(appRoot))
	}

	return nil
195 196 197 198 199 200
}

func specPath(appRoot string) string {
	return filepath.Join(appRoot, appYamlName)
}

201 202 203 204
func overridePath(appRoot string) string {
	return filepath.Join(appRoot, overrideYamlName)
}

205
// RepositorySpec defines the spec for the upstream repository of this project.
Jessica Yuen's avatar
Jessica Yuen committed
206 207 208 209 210
type RepositorySpec struct {
	Type string `json:"type"`
	URI  string `json:"uri"`
}

211
// RegistryConfig defines the spec for a registry. A registry is a collection
212
// of library parts.
213
type RegistryConfig struct {
bryanl's avatar
bryanl committed
214 215 216
	// Name is the user defined name of a registry.
	Name string `json:"-"`
	// Protocol is the registry protocol for this registry. Currently supported
217
	// values are `github`, `fs`, `helm`.
bryanl's avatar
bryanl committed
218 219 220
	Protocol string `json:"protocol"`
	// URI is the location of the registry.
	URI string `json:"uri"`
221 222 223 224

	isOverride bool
}

225 226
// IsOverride is true if this RegistryConfig is an override.
func (r *RegistryConfig) IsOverride() bool {
227
	return r.isOverride
Jessica Yuen's avatar
Jessica Yuen committed
228 229
}

230 231
// RegistryConfigs is a map of the registry name to a RegistryConfig.
type RegistryConfigs map[string]*RegistryConfig
Jessica Yuen's avatar
Jessica Yuen committed
232

233
// UnmarshalJSON implements the json.Unmarshaler interface.
234
// Our goal is to populate the Name field of RegistryConfig
235
// objects according to they key name in the registries map.
236 237
func (r *RegistryConfigs) UnmarshalJSON(b []byte) error {
	registries := make(map[string]*RegistryConfig)
238 239 240 241 242 243 244 245 246
	if err := json.Unmarshal(b, &registries); err != nil {
		return err
	}

	// Set Name fields according to map keys
	for k, v := range registries {
		v.Name = k
	}

247
	*r = RegistryConfigs(registries)
248 249 250
	return nil
}

251 252
// EnvironmentConfigs contains one or more EnvironmentConfig.
type EnvironmentConfigs map[string]*EnvironmentConfig
Jessica Yuen's avatar
Jessica Yuen committed
253

254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
// UnmarshalJSON implements the json.Unmarshaler interface.
// Our goal is to populate the Name field of EnvironmentConfig
// objects according to they key name in the environments map.
func (e *EnvironmentConfigs) UnmarshalJSON(b []byte) error {
	envs := make(map[string]*EnvironmentConfig)
	if err := json.Unmarshal(b, &envs); err != nil {
		return err
	}

	// Set Name fields according to map keys
	for k, v := range envs {
		v.Name = k
	}

	*e = EnvironmentConfigs(envs)
	return nil
}

// EnvironmentConfig contains the specification for ksonnet environments.
type EnvironmentConfig struct {
274 275
	// Name is the user defined name of an environment
	Name string `json:"-"`
bryanl's avatar
bryanl committed
276
	// KubernetesVersion is the kubernetes version the targeted cluster is
277 278 279 280 281 282 283 284 285 286
	// running on.
	KubernetesVersion string `json:"k8sVersion"`
	// Path is the relative project path containing metadata for this
	// environment.
	Path string `json:"path"`
	// Destination stores the cluster address that this environment points to.
	Destination *EnvironmentDestinationSpec `json:"destination"`
	// Targets contain the relative component paths that this environment
	// wishes to deploy on it's destination.
	Targets []string `json:"targets,omitempty"`
bryanl's avatar
bryanl committed
287 288 289 290

	isOverride bool
}

bryanl's avatar
bryanl committed
291
// MakePath return the absolute path to the environment directory.
292
func (e *EnvironmentConfig) MakePath(rootPath string) string {
bryanl's avatar
bryanl committed
293 294 295 296 297 298
	return filepath.Join(
		rootPath,
		EnvironmentDirName,
		filepath.FromSlash(e.Path))
}

299 300
// IsOverride is true if this EnvironmentConfig is an override.
func (e *EnvironmentConfig) IsOverride() bool {
bryanl's avatar
bryanl committed
301
	return e.isOverride
Jessica Yuen's avatar
Jessica Yuen committed
302 303 304
}

// EnvironmentDestinationSpec contains the specification for the cluster
305
// address that the environment points to.
Jessica Yuen's avatar
Jessica Yuen committed
306
type EnvironmentDestinationSpec struct {
307 308 309 310
	// Server is the Kubernetes server that the cluster is running on.
	Server string `json:"server"`
	// Namespace is the namespace of the Kubernetes server that targets should
	// be deployed to. This is "default", if not specified.
Jessica Yuen's avatar
Jessica Yuen committed
311 312 313
	Namespace string `json:"namespace"`
}

314 315
// LibraryConfig is the specification for a library part.
type LibraryConfig struct {
Jessica Yuen's avatar
Jessica Yuen committed
316 317
	Name       string          `json:"name"`
	Registry   string          `json:"registry"`
bryanl's avatar
bryanl committed
318
	Version    string          `json:"version"`
bryanl's avatar
bryanl committed
319
	GitVersion *GitVersionSpec `json:"gitVersion,omitempty"`
Jessica Yuen's avatar
Jessica Yuen committed
320 321
}

322
// GitVersionSpec is the specification for a Registry's Git Version.
Jessica Yuen's avatar
Jessica Yuen committed
323 324 325 326 327
type GitVersionSpec struct {
	RefSpec   string `json:"refSpec"`
	CommitSHA string `json:"commitSha"`
}

328 329
// LibraryConfigs is a mapping of a library configurations by name.
type LibraryConfigs map[string]*LibraryConfig
Jessica Yuen's avatar
Jessica Yuen committed
330

331
// ContributorSpec is a specification for the project contributors.
Jessica Yuen's avatar
Jessica Yuen committed
332 333 334 335 336
type ContributorSpec struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}

337
// ContributorSpecs is a list of 0 or more contributors.
Jessica Yuen's avatar
Jessica Yuen committed
338 339
type ContributorSpecs []*ContributorSpec

340
// Marshal converts a app.Spec into bytes for file writing.
341
func (s *Spec) Marshal() ([]byte, error) {
342
	return yaml.Marshal(s)
343 344
}

345 346 347
// RegistryConfig returns a populated RegistryConfig given a registry name.
func (s *Spec) RegistryConfig(name string) (*RegistryConfig, bool) {
	cfg, ok := s.Registries[name]
348
	if ok {
349 350 351 352 353
		// Verify map name matches the name in configuration. These should always match.
		if cfg.Name != name {
			log.WithField("action", "app.Spec.RegistryConfig").Warnf("registry configuration name mismatch: %v vs. %v", cfg.Name, name)
			cfg.Name = name
		}
354
	}
355
	return cfg, ok
356 357
}

358
// AddRegistryConfig adds the RegistryConfig to the app spec.
359 360
func (s *Spec) AddRegistryConfig(cfg *RegistryConfig) error {
	if cfg.Name == "" {
361
		return ErrRegistryNameInvalid
362 363
	}

364
	if _, exists := s.Registries[cfg.Name]; exists {
365
		return ErrRegistryExists
366 367
	}

368
	s.Registries[cfg.Name] = cfg
369
	return nil
370 371
}

372
func (s *Spec) validate() error {
373 374 375 376 377
	if s.Contributors == nil {
		s.Contributors = ContributorSpecs{}
	}

	if s.Registries == nil {
378
		s.Registries = RegistryConfigs{}
379 380 381
	}

	if s.Libraries == nil {
382
		s.Libraries = LibraryConfigs{}
383 384 385
	}

	if s.Environments == nil {
386
		s.Environments = EnvironmentConfigs{}
387 388
	}

389 390 391 392
	if s.APIVersion == "0.0.0" {
		return errors.New("invalid version")
	}

393 394 395 396
	compatVer, _ := semver.Make(DefaultAPIVersion)
	ver, err := semver.Make(s.APIVersion)
	if err != nil {
		return errors.Wrap(err, "Failed to parse version in app spec")
397 398 399
	}

	if compatVer.Compare(ver) < 0 {
400 401 402 403 404 405 406 407 408
		return fmt.Errorf(
			"Current app uses unsupported spec version '%s' (this client only supports %s)",
			s.APIVersion,
			DefaultAPIVersion)
	}

	return nil
}

409 410 411
// GetEnvironmentConfigs returns all environment specifications.
// TODO: Consider returning copies instead of originals
func (s *Spec) GetEnvironmentConfigs() EnvironmentConfigs {
412 413 414 415 416 417 418
	for k, v := range s.Environments {
		v.Name = k
	}

	return s.Environments
}

419 420 421 422
// GetEnvironmentConfig returns the environment specification for the environment.
// TODO: Consider returning copies instead of originals
func (s *Spec) GetEnvironmentConfig(name string) (*EnvironmentConfig, bool) {
	env, ok := s.Environments[name]
Jessica Yuen's avatar
Jessica Yuen committed
423
	if ok {
424
		env.Name = name
Jessica Yuen's avatar
Jessica Yuen committed
425
	}
426
	return env, ok
427 428
}

429
// AddEnvironmentConfig adds an EnvironmentConfig to the list of EnvironmentConfigs.
Jessica Yuen's avatar
Jessica Yuen committed
430
// This is equivalent to registering the environment for a ksonnet app.
431 432
func (s *Spec) AddEnvironmentConfig(env *EnvironmentConfig) error {
	if env.Name == "" {
Jessica Yuen's avatar
Jessica Yuen committed
433 434
		return ErrEnvironmentNameInvalid
	}
435

436
	if _, ok := s.Environments[env.Name]; ok {
Jessica Yuen's avatar
Jessica Yuen committed
437 438
		return ErrEnvironmentExists
	}
439

440
	s.Environments[env.Name] = env
Jessica Yuen's avatar
Jessica Yuen committed
441
	return nil
442 443
}

444 445
// DeleteEnvironmentConfig removes the environment specification from the app spec.
func (s *Spec) DeleteEnvironmentConfig(name string) error {
Jessica Yuen's avatar
Jessica Yuen committed
446 447
	delete(s.Environments, name)
	return nil
448
}
449

450
// UpdateEnvironmentConfig updates the environment with the provided name to the
451
// specified spec.
452 453
func (s *Spec) UpdateEnvironmentConfig(name string, env *EnvironmentConfig) error {
	if env.Name == "" {
454 455 456
		return ErrEnvironmentNameInvalid
	}

457 458
	_, ok := s.Environments[name]
	if !ok {
459
		return errors.Errorf("Environment with name %q does not exist", name)
460 461
	}

462 463
	if name != env.Name {
		if err := s.DeleteEnvironmentConfig(name); err != nil {
464 465 466 467
			return err
		}
	}

468
	s.Environments[env.Name] = env
469 470
	return nil
}