Example #1
0
// Get returns the frontend record for the given id.
// If the ID is not found, an IDNotFoundError is returned.
func (eb *etcdBackend) Get(id string) (api.FrontendRecord, error) {
	if err := validateID(id); err != nil {
		return api.FrontendRecord{}, maskAny(err)
	}
	etcdPath := path.Join(eb.prefix, frontEndPrefix, id)
	kAPI := client.NewKeysAPI(eb.client)
	options := &client.GetOptions{
		Recursive: false,
		Sort:      false,
	}
	resp, err := kAPI.Get(context.Background(), etcdPath, options)
	if isEtcdError(err, client.ErrorCodeKeyNotFound) {
		return api.FrontendRecord{}, maskAny(errgo.WithCausef(nil, api.IDNotFoundError, "ID '%s' not found", id))
	}
	if err != nil {
		eb.Logger.Warningf("ETCD error in Get: %#v", err)
		return api.FrontendRecord{}, maskAny(err)
	}
	if resp.Node == nil {
		return api.FrontendRecord{}, maskAny(errgo.WithCausef(nil, api.IDNotFoundError, "ID '%s' not found", id))
	}
	rawJSON := resp.Node.Value
	record := api.FrontendRecord{}
	if err := json.Unmarshal([]byte(rawJSON), &record); err != nil {
		return api.FrontendRecord{}, maskAny(fmt.Errorf("Cannot unmarshal registration of %s", id))
	}

	return record, nil
}
Example #2
0
// Restart behaves as `systemctl restart <unit>`
func (sdc *SystemdClient) Restart(unit string) error {
	sdc.Logger.Debugf("restarting %s", unit)

	conn, err := dbus.New()
	if err != nil {
		return maskAny(err)
	}

	responseChan := make(chan string, 1)
	if _, err := conn.RestartUnit(unit, "replace", responseChan); err != nil {
		sdc.Logger.Errorf("restarting %s failed: %#v", unit, err)
		return maskAny(err)
	}

	select {
	case res := <-responseChan:
		switch res {
		case "done":
			return nil
		case "failed":
			// We need a start considered to be failed, when the unit is already running.
			return nil
		case "canceled", "timeout", "dependency", "skipped":
			return maskAny(errgo.WithCausef(nil, SystemdError, res))
		default:
			// that should never happen
			sdc.Logger.Errorf("unexpected systemd response: '%s'", res)
			return maskAny(errgo.WithCausef(nil, SystemdError, res))
		}
	case <-time.After(jobTimeout):
		return maskAny(errgo.WithCausef(nil, SystemdError, "job timeout"))
	}

	return nil
}
Example #3
0
// Validate checks the given object for invalid values.
func (r FrontendSelectorRecord) Validate() error {
	if r.Weight < 0 || r.Weight > 100 {
		return maskAny(errgo.WithCausef(nil, ValidationError, "weight must be between 0-100"))
	}
	if r.ServicePort < 0 || r.ServicePort > maxPort {
		return maskAny(errgo.WithCausef(nil, ValidationError, "port must be between 0-%d", maxPort))
	}
	if r.FrontendPort < 0 || r.FrontendPort > maxPort {
		return maskAny(errgo.WithCausef(nil, ValidationError, "frontend-port must be between 0-%d", maxPort))
	}
	if r.Domain == "" && r.PathPrefix == "" && r.FrontendPort == 0 {
		return maskAny(errgo.WithCausef(nil, ValidationError, "domain, path-prefix or frontend-port must be set"))
	}
	for _, ur := range r.Users {
		if err := ur.Validate(); err != nil {
			return maskAny(err)
		}
	}
	for _, rr := range r.RewriteRules {
		if err := rr.Validate(); err != nil {
			return maskAny(err)
		}
	}
	return nil
}
Example #4
0
File: job.go Project: pulcy/j2
// Check for errors
func (j *Job) Validate() error {
	if err := j.Name.Validate(); err != nil {
		return maskAny(err)
	}
	if len(j.Groups) == 0 {
		return maskAny(errgo.WithCausef(nil, ValidationError, "job has no groups"))
	}
	for i, tg := range j.Groups {
		err := tg.Validate()
		if err != nil {
			return maskAny(err)
		}
		for k := i + 1; k < len(j.Groups); k++ {
			if j.Groups[k].Name == tg.Name {
				return maskAny(errgo.WithCausef(nil, ValidationError, "job has duplicate taskgroup %s", tg.Name))
			}
		}
	}
	if err := j.Constraints.Validate(); err != nil {
		return maskAny(err)
	}
	if err := j.Dependencies.Validate(); err != nil {
		return maskAny(err)
	}
	return nil
}
Example #5
0
// Check for configuration errors
func (tg *TaskGroup) Validate() error {
	if err := tg.Name.Validate(); err != nil {
		return maskAny(err)
	}
	if tg.Count <= 0 {
		return maskAny(errgo.WithCausef(nil, ValidationError, "group %s count <= 0", tg.Name))
	}
	if len(tg.Tasks) == 0 {
		return maskAny(errgo.WithCausef(nil, ValidationError, "group %s has no tasks", tg.Name))
	}
	for i, t := range tg.Tasks {
		err := t.Validate()
		if err != nil {
			return maskAny(err)
		}
		for j := i + 1; j < len(tg.Tasks); j++ {
			if tg.Tasks[j].Name == t.Name {
				return maskAny(errgo.WithCausef(nil, ValidationError, "group %s has duplicate task %s", tg.Name, t.Name))
			}
		}
	}
	if err := tg.Constraints.Validate(); err != nil {
		return maskAny(err)
	}
	if err := tg.RestartPolicy.Validate(); err != nil {
		return maskAny(err)
	}
	return nil
}
Example #6
0
// Validate checks the given object for invalid values.
func (r UserRecord) Validate() error {
	if r.Name == "" {
		return maskAny(errgo.WithCausef(nil, ValidationError, "name must be set"))
	}
	if r.PasswordHash == "" {
		return maskAny(errgo.WithCausef(nil, ValidationError, "pwhash must be set"))
	}
	return nil
}
Example #7
0
File: secret.go Project: pulcy/j2
// Validate checks the values of the given secret.
// If ok, return nil, otherwise returns an error.
func (s *Secret) Validate() error {
	if s.Path == "" {
		return maskAny(errgo.WithCausef(nil, ValidationError, "path is empty"))
	}
	if s.Environment == "" && s.File == "" {
		return maskAny(errgo.WithCausef(nil, ValidationError, "environment and file is empty"))
	}
	return nil
}
Example #8
0
// Validate checks the given object for invalid values.
func (r RewriteRule) Validate() error {
	if r.PathPrefix == "" && r.RemovePathPrefix == "" && r.Domain == "" {
		return maskAny(errgo.WithCausef(nil, ValidationError, "at least 1 property must be set"))
	}
	if r.PathPrefix != "" && r.RemovePathPrefix != "" {
		return maskAny(errgo.WithCausef(nil, ValidationError, "path-prefix and remove-path-prefix cannot be set both"))
	}
	return nil
}
Example #9
0
// SendMessage reads the configuration file, and posts a message about Kocho's invocation to Slack.
func SendMessage(version, build string) error {
	expanded, err := homedir.Expand(configPath)
	if err != nil {
		return err
	}
	if _, err := os.Stat(expanded); os.IsNotExist(err) {
		return errgo.Mask(ErrNotConfigured, errgo.Any)
	}

	slackConfiguration := SlackConfiguration{
		NotificationUsername: "******",
		EmojiIcon:            ":robot_face:",
	}

	configFile, err := os.Open(expanded)
	if err != nil {
		return errgo.WithCausef(err, ErrInvalidConfiguration, "couldn't open Slack configuration file")
	}
	defer configFile.Close()

	if err := json.NewDecoder(configFile).Decode(&slackConfiguration); err != nil {
		return errgo.WithCausef(err, ErrInvalidConfiguration, "couldn't decode Slack configuration")
	}

	client := slack.New(slackConfiguration.Token)

	params := slack.PostMessageParameters{}
	params.Attachments = []slack.Attachment{
		slack.Attachment{
			Color: "#2484BE",
			Text:  fmt.Sprintf("*Kocho*: %s ran `%s`", slackConfiguration.Username, strings.Join(os.Args, " ")),
			Fields: []slack.AttachmentField{
				slack.AttachmentField{
					Title: "Kocho Version",
					Value: version,
					Short: true,
				},
				slack.AttachmentField{
					Title: "Kocho Build",
					Value: build,
					Short: true,
				},
			},
			MarkdownIn: []string{"text"},
		},
	}
	params.Username = slackConfiguration.NotificationUsername
	params.IconEmoji = slackConfiguration.EmojiIcon

	if _, _, err := client.PostMessage(slackConfiguration.NotificationChannel, "", params); err != nil {
		return err
	}

	return nil
}
Example #10
0
func runKillInstance(args []string) (exit int) {
	if len(args) != 2 {
		return exitError("wrong number of arguments. Usage: kocho kill-instance <swarm> <instance>")
	}
	swarmName := args[0]
	instanceID := args[1]

	s, err := swarmService.Get(swarmName, swarm.AWS)
	if err != nil {
		return exitError(fmt.Sprintf("couldn't get instances of swarm: %s", swarmName), err)
	}

	instances, err := s.GetInstances()
	if err != nil {
		return exitError(err)
	}

	killableInstance, err := swarmtypes.FindInstanceById(instances, instanceID)
	if err != nil {
		return exitError(errgo.WithCausef(err, nil, "failed to find provided instance: %s", instanceID))
	}

	runningInstances := swarmtypes.FilterInstanceById(instances, instanceID)
	if len(runningInstances) == 0 {
		return exitError(errgo.Newf("no more instances left in swarm %s. Cannot update Fleet DNS entry", swarmName))
	}

	if !ignoreQuorumCheck {
		etcdQuorumID, err := ssh.GetEtcd2MemberName(killableInstance.PublicIPAddress)
		if err != nil {
			return exitError(errgo.WithCausef(err, nil, "ssh: failed to check quorum member list: %v", err))
		}

		if etcdQuorumID != "" {
			return exitError(errgo.Newf("Instance %s seems to be part of the etcd quorum. Please remove it beforehand. See %s", killableInstance.Id, etcdDocsLink))
		}
	}

	if err = s.KillInstance(killableInstance); err != nil {
		return exitError(errgo.WithCausef(err, nil, "failed to kill instance: %s", instanceID))
	}

	if changed, err := dns.Update(dnsService, viperConfig.getDNSNamingPattern(), s, runningInstances); err != nil {
		return exitError(errgo.WithCausef(err, nil, "failed to update dns records"))
	} else if !changed {
		return exitError(errgo.Newf("DNS not changed. Couldn't find valid publid DNS name"))
	}

	fmt.Printf(killInstanceSuccessMessage, killableInstance.Id, etcdDocsLink)

	fireNotification()

	return 0
}
Example #11
0
// Validate checks the values of the given constraint.
// If ok, return nil, otherwise returns an error.
func (c Constraint) Validate() error {
	if c.Attribute == "" {
		return errgo.WithCausef(nil, ValidationError, "attribute cannot be empty")
	}
	switch c.Operator {
	case "", OperatorEqual, OperatorNotEqual:
		// Ok
	default:
		return errgo.WithCausef(nil, ValidationError, "unknown operator '%s'", c.Operator)
	}
	return nil
}
Example #12
0
File: frontend.go Project: pulcy/j2
// Validate checks the values of the given frontend.
// If ok, return nil, otherwise returns an error.
func (f PrivateFrontEnd) Validate() error {
	if f.Weight < 0 || f.Weight > 100 {
		return errgo.WithCausef(nil, ValidationError, "weight must be between 0 and 100")
	}
	switch f.Mode {
	case "", "http", "tcp":
		// OK
	default:
		return errgo.WithCausef(nil, ValidationError, "mode must be http or tcp")
	}
	return nil
}
Example #13
0
File: link.go Project: pulcy/j2
func (l Link) Validate() error {
	if err := l.Target.Validate(); err != nil {
		return maskAny(err)
	}
	if err := l.Type.Validate(); err != nil {
		return maskAny(err)
	}
	if len(l.Ports) == 0 && l.Type.IsTCP() {
		return maskAny(errgo.WithCausef(nil, ValidationError, "specify at least one port in a tcp link"))
	}
	if len(l.Ports) != 0 && !l.Type.IsTCP() {
		return maskAny(errgo.WithCausef(nil, ValidationError, "ports are not allowed in non-tcp links"))
	}
	return nil
}
Example #14
0
File: parse.go Project: pulcy/j2
// parse a private frontend
func (f *PrivateFrontEnd) parse(obj *ast.ObjectType) error {
	// Build the frontend
	excludedKeys := []string{
		"user",
	}
	defaultValues := map[string]interface{}{
		"port": 80,
	}
	if err := hclutil.Decode(obj, excludedKeys, defaultValues, f); err != nil {
		return maskAny(err)
	}
	if o := obj.List.Filter("user"); len(o.Items) > 0 {
		for _, o := range o.Children().Items {
			if obj, ok := o.Val.(*ast.ObjectType); ok {
				n := o.Keys[0].Token.Value().(string)
				u := User{Name: n}
				if err := u.parse(obj); err != nil {
					return maskAny(err)
				}
				f.Users = append(f.Users, u)
			} else {
				return maskAny(errgo.WithCausef(nil, ValidationError, "user of frontend %#v is not an object or array", f))
			}
		}
	}

	return nil
}
Example #15
0
// GithubLogin performs a standard Github authentication and initializes the vaultClient with the resulting token.
func (s *VaultService) GithubLogin(data GithubLoginData) (*AuthenticatedVaultClient, error) {
	// Perform login
	vaultClient, address, err := s.newUnsealedClient()
	if err != nil {
		return nil, maskAny(err)
	}
	vaultClient.ClearToken()
	logical := vaultClient.Logical()
	loginData := make(map[string]interface{})
	loginData["token"] = data.GithubToken
	if data.Mount == "" {
		data.Mount = "github"
	}
	path := fmt.Sprintf("auth/%s/login", data.Mount)
	s.log.Debugf("write loginData at %s", address)
	if loginSecret, err := logical.Write(path, loginData); err != nil {
		return nil, maskAny(err)
	} else if loginSecret.Auth == nil {
		return nil, maskAny(errgo.WithCausef(nil, VaultError, "missing authentication in secret response"))
	} else {
		// Use token
		vaultClient.SetToken(loginSecret.Auth.ClientToken)
	}

	// We're done
	return s.newAuthenticatedClient(vaultClient), nil
}
Example #16
0
File: mutex.go Project: pulcy/robin
// newMutex creates and initializes a new GlobalMutex.
func newMutex(name string, ttl time.Duration, service mutexService) (*GlobalMutex, error) {
	if name == "" {
		return nil, errgo.WithCausef(nil, InvalidArgumentError, "name empty")
	}
	if ttl <= 0 {
		return nil, errgo.WithCausef(nil, InvalidArgumentError, "ttl <= 0")
	}
	if service == nil {
		return nil, errgo.WithCausef(nil, InvalidArgumentError, "service nil")
	}
	return &GlobalMutex{
		name:    name,
		ttl:     ttl,
		service: service,
	}, nil
}
Example #17
0
// getEnv loads an environment value and returns an error if it is empty.
func (jf *jobFunctions) getEnv(key string) (string, error) {
	value := os.Getenv(key)
	if value == "" {
		return "", errgo.WithCausef(nil, ValidationError, "Missing environment variables '%s'", key)
	}
	return value, nil
}
Example #18
0
// getOpt loads an option with given key and returns an error the option does not exist.
func (jf *jobFunctions) getOpt(key string) (string, error) {
	value, ok := jf.options.Get(key)
	if !ok {
		value, ok = jf.cluster.DefaultOptions.Get(key)
		if !ok {
			switch key {
			case "domain":
				return jf.cluster.Domain, nil
			case "stack":
				return jf.cluster.Stack, nil
			case "tunnel":
				return jf.cluster.Tunnel, nil
			case "instance-count":
				return strconv.Itoa(jf.cluster.InstanceCount), nil
			default:
				return "", errgo.WithCausef(nil, ValidationError, "Missing option '%s'", key)
			}
		}
	}
	if result, err := formatOptionValue(value, false); err != nil {
		return "", maskAny(err)
	} else {
		return result, nil
	}
}
Example #19
0
File: vault.go Project: pulcy/j2
// GithubLogin performs a standard Github authentication and initializes the vaultClient with the resulting token.
func (s *Vault) GithubLogin(data GithubLoginData) error {
	// Read token
	var err error
	data.GithubToken, err = s.readGithubToken(data)
	if err != nil {
		return maskAny(err)
	}
	// Perform login
	s.vaultClient.ClearToken()
	logical := s.vaultClient.Logical()
	loginData := make(map[string]interface{})
	loginData["token"] = data.GithubToken
	if data.Mount == "" {
		data.Mount = "github"
	}
	path := fmt.Sprintf("auth/%s/login", data.Mount)
	if loginSecret, err := logical.Write(path, loginData); err != nil {
		return maskAny(err)
	} else if loginSecret.Auth == nil {
		return maskAny(errgo.WithCausef(nil, VaultError, "missing authentication in secret response"))
	} else {
		// Use token
		s.vaultClient.SetToken(loginSecret.Auth.ClientToken)
	}

	// We're done
	return nil
}
Example #20
0
// newUnsealedClient creates the first single vault client that resolves to an unsealed vault instance.
func (s *VaultService) newUnsealedClient() (*api.Client, string, error) {
	clients, err := s.newClients()
	if err != nil {
		return nil, "", maskAny(err)
	}
	for _, client := range clients {
		// Check seal status
		status, err := client.Client.Sys().SealStatus()
		if err != nil {
			s.log.Debugf("vault at %s cannot be reached: %s", client.Address, Describe(err))
			continue
		} else if status.Sealed {
			s.log.Warningf("Vault at %s is sealed", client.Address)
			continue
		}

		// Check leader status
		resp, err := client.Client.Sys().Leader()
		if err != nil {
			s.log.Debugf("vault at %s cannot be reached: %s", client.Address, Describe(err))
			continue
		} else if resp.HAEnabled && !resp.IsSelf {
			s.log.Debugf("vault at %s is not the leader", client.Address)
			continue
		}

		s.log.Debugf("found unsealed vault client at %s", client.Address)
		return client.Client, client.Address, nil
	}
	return nil, "", maskAny(errgo.WithCausef(nil, VaultError, "no unsealed vault instance found"))
}
Example #21
0
File: volume.go Project: pulcy/j2
// MarshalJSON creates a json representation of a given volume
func (v Volume) MarshalJSON() ([]byte, error) {
	str := v.String()
	if str == "" {
		return nil, maskAny(errgo.WithCausef(nil, ValidationError, "invalid type '%s'", v.Type))
	}
	return json.Marshal(str)
}
Example #22
0
File: task.go Project: pulcy/j2
// newEngine creates a new Engine for the given task.
func newEngine(t *jobs.Task, ctx generatorContext) (engine.Engine, error) {
	provider := extpoints.EngineProviders.Lookup(t.Engine.String())
	if provider == nil {
		return nil, maskAny(errgo.WithCausef(nil, ValidationError, "unknown engine type '%s'", t.Engine))
	}
	return provider.NewEngine(ctx.Cluster), nil
}
Example #23
0
func formatOptionValue(value interface{}, quote bool) (string, error) {
	if s, ok := value.(string); ok {
		if quote {
			return strconv.Quote(s), nil
		}
		return s, nil
	}
	if l, ok := value.([]interface{}); ok {
		var result []string
		for _, e := range l {
			fe, err := formatOptionValue(e, true)
			if err != nil {
				return "", maskAny(err)
			}
			result = append(result, fe)
		}
		return "[" + strings.Join(result, ", ") + "]", nil
	}
	if m, ok := value.(map[string]interface{}); ok {
		var result []string
		for k, v := range m {
			fv, err := formatOptionValue(v, true)
			if err != nil {
				return "", maskAny(err)
			}
			result = append(result, fmt.Sprintf("%s = %s", k, fv))
		}
		return "{\n" + strings.Join(result, "\n") + "}", nil
	}
	return "", maskAny(errgo.WithCausef(nil, ValidationError, "Unknown value type: %v", value))
}
Example #24
0
File: parse.go Project: pulcy/quark
// parse a QuarkOptions
func (options *QuarkOptions) parse(obj *ast.ObjectType, c Cluster) error {
	// Parse the object
	excludeList := []string{
		"profile",
	}
	values, err := decodeIntoMap(obj, excludeList, nil)
	if err != nil {
		return maskAny(err)
	}
	options.DefaultValues = values

	// Parse profiles
	if o := obj.List.Filter("profile"); len(o.Items) > 0 {
		for _, o := range o.Children().Items {
			if obj, ok := o.Val.(*ast.ObjectType); ok {
				p := Profile{}
				n := o.Keys[0].Token.Value().(string)
				if err := p.parse(obj); err != nil {
					return maskAny(err)
				}
				p.Name = n
				options.Profiles = append(options.Profiles, p)
			} else {
				return maskAny(errgo.WithCausef(nil, ValidationError, "profile is not an object"))
			}
		}
	}

	return nil
}
Example #25
0
// Add adds a given frontend record with given ID to the list of frontends.
// If the given ID already exists, a DuplicateIDError is returned.
func (eb *etcdBackend) Add(id string, record api.FrontendRecord) error {
	if err := validateID(id); err != nil {
		return maskAny(err)
	}
	if err := record.Validate(); err != nil {
		return maskAny(err)
	}
	etcdPath := path.Join(eb.prefix, frontEndPrefix, id)
	kAPI := client.NewKeysAPI(eb.client)
	options := &client.SetOptions{
		PrevExist: client.PrevNoExist,
	}
	rawJSON, err := json.Marshal(record)
	if err != nil {
		return maskAny(err)
	}
	if _, err := kAPI.Set(context.Background(), etcdPath, string(rawJSON), options); isEtcdError(err, client.ErrorCodeNodeExist) {
		return maskAny(errgo.WithCausef(nil, api.DuplicateIDError, "Duplicate ID '%s'", id))
	} else if err != nil {
		eb.Logger.Warningf("ETCD error in Add: %#v", err)
		return maskAny(err)
	}

	return nil
}
Example #26
0
File: parse.go Project: pulcy/j2
// ParseJob takes input from a given reader and parses it into a Job.
func parseJob(input []byte, jf *jobFunctions) (*Job, error) {
	// Create a template, add the function map, and parse the text.
	tmpl, err := template.New("job").Funcs(jf.Functions()).Parse(string(input))
	if err != nil {
		return nil, maskAny(err)
	}

	// Run the template to verify the output.
	buffer := &bytes.Buffer{}
	err = tmpl.Execute(buffer, jf.Options())
	if err != nil {
		return nil, maskAny(err)
	}

	// Parse the input
	root, err := hcl.Parse(buffer.String())
	if err != nil {
		return nil, maskAny(err)
	}
	// Top-level item should be a list
	list, ok := root.Node.(*ast.ObjectList)
	if !ok {
		return nil, errgo.New("error parsing: root should be an object")
	}

	// Parse hcl into Job
	job := &Job{}
	matches := list.Filter("job")
	if len(matches.Items) == 0 {
		return nil, maskAny(errgo.WithCausef(nil, ValidationError, "'job' stanza not found"))
	}
	if err := job.parse(matches); err != nil {
		return nil, maskAny(err)
	}

	// Link internal structures
	job.prelink()

	// Set defaults
	job.setDefaults(jf.cluster)

	// Replace variables
	if err := job.replaceVariables(); err != nil {
		return nil, maskAny(err)
	}

	// Sort internal structures and make final links
	job.link()

	// Optimize job for cluster
	job.optimizeFor(jf.cluster)

	// Validate the job
	if err := job.Validate(); err != nil {
		return nil, maskAny(err)
	}

	return job, nil
}
Example #27
0
// Validate returns an error if the given network type is invalid.
// Returns nil on ok.
func (nt NetworkType) Validate() error {
	switch nt {
	case NetworkTypeDefault, NetworkTypeHost, NetworkTypeWeave:
		return nil
	default:
		return maskAny(errgo.WithCausef(nil, ValidationError, "unknown network type '%s'", string(nt)))
	}
}
Example #28
0
File: job.go Project: pulcy/j2
// TaskGroup gets a taskgroup by the given name
func (j *Job) TaskGroup(name TaskGroupName) (*TaskGroup, error) {
	for _, tg := range j.Groups {
		if tg.Name == name {
			return tg, nil
		}
	}
	return nil, maskAny(errgo.WithCausef(nil, TaskGroupNotFoundError, name.String()))
}
Example #29
0
// Validate checks if a link name follows a valid format
func (lt LinkType) Validate() error {
	switch string(lt) {
	case "http", "tcp", "":
		return nil
	default:
		return maskAny(errgo.WithCausef(nil, ValidationError, "invalid link type '%s'", string(lt)))
	}
}
Example #30
0
File: job.go Project: pulcy/j2
// Dependency gets a dependency by the given name
func (j *Job) Dependency(name LinkName) (Dependency, error) {
	for _, d := range j.Dependencies {
		if d.Name == name {
			return d, nil
		}
	}
	return Dependency{}, maskAny(errgo.WithCausef(nil, DependencyNotFoundError, name.String()))
}