Beispiel #1
0
func (e *DNSName) Find(c *fi.Context) (*DNSName, error) {
	cloud := c.Cloud.(*awsup.AWSCloud)

	findName := fi.StringValue(e.Name)
	if findName == "" {
		return nil, nil
	}
	findName = strings.TrimSuffix(findName, ".")

	findType := fi.StringValue(e.ResourceType)
	if findType == "" {
		return nil, nil
	}

	request := &route53.ListResourceRecordSetsInput{
		HostedZoneId: e.Zone.ID,
		// TODO: Start at correct name?
	}

	var found *route53.ResourceRecordSet

	err := cloud.Route53.ListResourceRecordSetsPages(request, func(p *route53.ListResourceRecordSetsOutput, lastPage bool) (shouldContinue bool) {
		for _, rr := range p.ResourceRecordSets {
			resourceType := aws.StringValue(rr.Type)
			name := aws.StringValue(rr.Name)

			glog.V(4).Infof("Found DNS resource %q %q", resourceType, name)

			if findType != resourceType {
				continue
			}

			name = strings.TrimSuffix(name, ".")

			if name == findName {
				found = rr
				break
			}
		}

		// TODO: Also exit if we are on the 'next' name?

		return found == nil
	})

	if err != nil {
		return nil, fmt.Errorf("error listing DNS ResourceRecords: %v", err)
	}

	if found == nil {
		return nil, nil
	}

	actual := &DNSName{}
	actual.Zone = e.Zone
	actual.Name = e.Name
	actual.ResourceType = e.ResourceType

	return actual, nil
}
Beispiel #2
0
func (_ *File) RenderCloudInit(t *cloudinit.CloudInitTarget, a, e, changes *File) error {
	dirMode := os.FileMode(0755)
	fileMode, err := fi.ParseFileMode(fi.StringValue(e.Mode), 0644)
	if err != nil {
		return fmt.Errorf("invalid file mode for %q: %q", e.Path, e.Mode)
	}

	if e.Type == FileType_Symlink {
		t.AddCommand(cloudinit.Always, "ln", "-s", fi.StringValue(e.Symlink), e.Path)
	} else if e.Type == FileType_Directory {
		parent := filepath.Dir(strings.TrimSuffix(e.Path, "/"))
		t.AddCommand(cloudinit.Once, "mkdir", "-p", "-m", fi.FileModeToString(dirMode), parent)
		t.AddCommand(cloudinit.Once, "mkdir", "-m", fi.FileModeToString(dirMode), e.Path)
	} else if e.Type == FileType_File {
		err = t.WriteFile(e.Path, e.Contents, fileMode, dirMode)
		if err != nil {
			return err
		}
	} else {
		return fmt.Errorf("File type=%q not valid/supported", e.Type)
	}

	if e.Owner != nil || e.Group != nil {
		t.Chown(e.Path, fi.StringValue(e.Owner), fi.StringValue(e.Group))
	}

	if e.OnChangeExecute != nil {
		t.AddCommand(cloudinit.Always, e.OnChangeExecute...)
	}

	return nil
}
Beispiel #3
0
func (e *LoadBalancer) Find(c *fi.Context) (*LoadBalancer, error) {
	cloud := c.Cloud.(*awsup.AWSCloud)

	elbName := fi.StringValue(e.ID)
	if elbName == "" {
		elbName = fi.StringValue(e.Name)
	}

	lb, err := findELB(cloud, elbName)
	if err != nil {
		return nil, err
	}
	if lb == nil {
		return nil, nil
	}

	actual := &LoadBalancer{}
	actual.Name = e.Name
	actual.ID = lb.LoadBalancerName
	actual.DNSName = lb.DNSName
	actual.HostedZoneId = lb.CanonicalHostedZoneNameID
	for _, subnet := range lb.Subnets {
		actual.Subnets = append(actual.Subnets, &Subnet{ID: subnet})
	}

	for _, sg := range lb.SecurityGroups {
		actual.SecurityGroups = append(actual.SecurityGroups, &SecurityGroup{ID: sg})
	}

	actual.Listeners = make(map[string]*LoadBalancerListener)

	for _, ld := range lb.ListenerDescriptions {
		l := ld.Listener
		loadBalancerPort := strconv.FormatInt(aws.Int64Value(l.LoadBalancerPort), 10)

		actualListener := &LoadBalancerListener{}
		actualListener.InstancePort = int(aws.Int64Value(l.InstancePort))
		actual.Listeners[loadBalancerPort] = actualListener
	}

	// Avoid spurious mismatches
	if subnetSlicesEqualIgnoreOrder(actual.Subnets, e.Subnets) {
		actual.Subnets = e.Subnets
	}
	if e.DNSName == nil {
		e.DNSName = actual.DNSName
	}
	if e.HostedZoneId == nil {
		e.HostedZoneId = actual.HostedZoneId
	}
	if e.ID == nil {
		e.ID = actual.ID
	}

	return actual, nil
}
Beispiel #4
0
func (e *DNSZone) findExisting(cloud *awsup.AWSCloud) (*route53.HostedZone, error) {
	findName := fi.StringValue(e.Name)
	if findName == "" {
		return nil, nil
	}
	if !strings.HasSuffix(findName, ".") {
		findName += "."
	}
	request := &route53.ListHostedZonesByNameInput{
		DNSName: aws.String(findName),
	}

	response, err := cloud.Route53.ListHostedZonesByName(request)
	if err != nil {
		return nil, fmt.Errorf("error listing DNS HostedZones: %v", err)
	}

	var zones []*route53.HostedZone
	for _, zone := range response.HostedZones {
		if aws.StringValue(zone.Name) == findName {
			zones = append(zones, zone)
		}
	}
	if len(zones) == 0 {
		return nil, nil
	}
	if len(zones) != 1 {
		return nil, fmt.Errorf("found multiple hosted zones matched name %q", findName)
	}

	return zones[0], nil
}
func (e *LoadBalancerHealthChecks) Find(c *fi.Context) (*LoadBalancerHealthChecks, error) {
	cloud := c.Cloud.(*awsup.AWSCloud)

	elbName := fi.StringValue(e.LoadBalancer.ID)

	lb, err := findELB(cloud, elbName)
	if err != nil {
		return nil, err
	}
	if lb == nil {
		return nil, nil
	}

	actual := &LoadBalancerHealthChecks{}
	actual.LoadBalancer = e.LoadBalancer

	if lb.HealthCheck != nil {
		actual.Target = lb.HealthCheck.Target
		actual.HealthyThreshold = lb.HealthCheck.HealthyThreshold
		actual.UnhealthyThreshold = lb.HealthCheck.UnhealthyThreshold
		actual.Interval = lb.HealthCheck.Interval
		actual.Timeout = lb.HealthCheck.Timeout
	}
	return actual, nil

}
Beispiel #6
0
func (_ *VPC) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *VPC) error {
	shared := fi.BoolValue(e.Shared)
	if shared {
		// Verify the VPC was found and matches our required settings
		if a == nil {
			return fmt.Errorf("VPC with id %q not found", fi.StringValue(e.ID))
		}

		if changes != nil && changes.EnableDNSSupport != nil {
			return fmt.Errorf("VPC with id %q was set to be shared, but did not have EnableDNSSupport=true", fi.StringValue(e.ID))
		}

		if changes != nil && changes.EnableDNSHostnames != nil {
			return fmt.Errorf("VPC with id %q was set to be shared, but did not have EnableDNSHostnames=true", fi.StringValue(e.ID))
		}

		return nil
	}

	if a == nil {
		glog.V(2).Infof("Creating VPC with CIDR: %q", *e.CIDR)

		request := &ec2.CreateVpcInput{
			CidrBlock: e.CIDR,
		}

		response, err := t.Cloud.EC2.CreateVpc(request)
		if err != nil {
			return fmt.Errorf("error creating VPC: %v", err)
		}

		e.ID = response.Vpc.VpcId
	}

	if changes.EnableDNSSupport != nil {
		request := &ec2.ModifyVpcAttributeInput{
			VpcId:            e.ID,
			EnableDnsSupport: &ec2.AttributeBooleanValue{Value: changes.EnableDNSSupport},
		}

		_, err := t.Cloud.EC2.ModifyVpcAttribute(request)
		if err != nil {
			return fmt.Errorf("error modifying VPC attribute: %v", err)
		}
	}

	if changes.EnableDNSHostnames != nil {
		request := &ec2.ModifyVpcAttributeInput{
			VpcId:              e.ID,
			EnableDnsHostnames: &ec2.AttributeBooleanValue{Value: changes.EnableDNSHostnames},
		}

		_, err := t.Cloud.EC2.ModifyVpcAttribute(request)
		if err != nil {
			return fmt.Errorf("error modifying VPC attribute: %v", err)
		}
	}

	return t.AddAWSTags(*e.ID, t.Cloud.BuildTags(e.Name))
}
Beispiel #7
0
func (s *DNSName) CheckChanges(a, e, changes *DNSName) error {
	if a == nil {
		if fi.StringValue(e.Name) == "" {
			return fi.RequiredField("Name")
		}
	}
	return nil
}
Beispiel #8
0
func (s *IAMInstanceProfile) CheckChanges(a, e, changes *IAMInstanceProfile) error {
	if a != nil {
		if fi.StringValue(e.Name) == "" {
			return fi.RequiredField("Name")
		}
	}
	return nil
}
Beispiel #9
0
func (_ *Package) RenderLocal(t *local.LocalTarget, a, e, changes *Package) error {
	if changes.Version != nil {
		glog.Infof("Installing package %q", e.Name)

		if e.Source != nil {
			// Install a deb
			local := path.Join(localPackageDir, e.Name)
			err := os.MkdirAll(localPackageDir, 0755)
			if err != nil {
				return fmt.Errorf("error creating directories %q: %v", path.Dir(local), err)
			}

			var hash *hashing.Hash
			if fi.StringValue(e.Hash) != "" {
				parsed, err := hashing.FromString(fi.StringValue(e.Hash))
				if err != nil {
					return fmt.Errorf("error paring hash: %v", err)
				}
				hash = parsed
			}
			_, err = fi.DownloadURL(fi.StringValue(e.Source), local, hash)
			if err != nil {
				return err
			}

			args := []string{"dpkg", "-i", local}
			glog.Infof("running command %s", args)
			cmd := exec.Command(args[0], args[1:]...)
			output, err := cmd.CombinedOutput()
			if err != nil {
				return fmt.Errorf("error installing package %q: %v: %s", e.Name, err, string(output))
			}
		} else {
			args := []string{"apt-get", "install", "--yes", e.Name}
			glog.Infof("running command %s", args)
			cmd := exec.Command(args[0], args[1:]...)
			output, err := cmd.CombinedOutput()
			if err != nil {
				return fmt.Errorf("error installing package %q: %v: %s", e.Name, err, string(output))
			}
		}
	}

	return nil
}
Beispiel #10
0
func (e *VPC) Find(c *fi.Context) (*VPC, error) {
	cloud := c.Cloud.(*awsup.AWSCloud)

	request := &ec2.DescribeVpcsInput{}

	if fi.StringValue(e.ID) != "" {
		request.VpcIds = []*string{e.ID}
	} else {
		request.Filters = cloud.BuildFilters(e.Name)
	}

	response, err := cloud.EC2.DescribeVpcs(request)
	if err != nil {
		return nil, fmt.Errorf("error listing VPCs: %v", err)
	}
	if response == nil || len(response.Vpcs) == 0 {
		return nil, nil
	}

	if len(response.Vpcs) != 1 {
		return nil, fmt.Errorf("found multiple VPCs matching tags")
	}
	vpc := response.Vpcs[0]
	actual := &VPC{
		ID:   vpc.VpcId,
		CIDR: vpc.CidrBlock,
		Name: findNameTag(vpc.Tags),
	}

	glog.V(4).Infof("found matching VPC %v", actual)

	if actual.ID != nil {
		request := &ec2.DescribeVpcAttributeInput{VpcId: actual.ID, Attribute: aws.String(ec2.VpcAttributeNameEnableDnsSupport)}
		response, err := cloud.EC2.DescribeVpcAttribute(request)
		if err != nil {
			return nil, fmt.Errorf("error querying for dns support: %v", err)
		}
		actual.EnableDNSSupport = response.EnableDnsSupport.Value
	}

	if actual.ID != nil {
		request := &ec2.DescribeVpcAttributeInput{VpcId: actual.ID, Attribute: aws.String(ec2.VpcAttributeNameEnableDnsHostnames)}
		response, err := cloud.EC2.DescribeVpcAttribute(request)
		if err != nil {
			return nil, fmt.Errorf("error querying for dns support: %v", err)
		}
		actual.EnableDNSHostnames = response.EnableDnsHostnames.Value
	}

	// Prevent spurious comparison failures
	actual.Shared = e.Shared
	if e.ID == nil {
		e.ID = actual.ID
	}

	return actual, nil
}
Beispiel #11
0
func (e *SSHKey) Find(c *fi.Context) (*SSHKey, error) {
	cloud := c.Cloud.(*awsup.AWSCloud)

	request := &ec2.DescribeKeyPairsInput{
		KeyNames: []*string{e.Name},
	}

	response, err := cloud.EC2.DescribeKeyPairs(request)
	if awsErr, ok := err.(awserr.Error); ok {
		if awsErr.Code() == "InvalidKeyPair.NotFound" {
			return nil, nil
		}
	}
	if err != nil {
		return nil, fmt.Errorf("error listing SSHKeys: %v", err)
	}

	if response == nil || len(response.KeyPairs) == 0 {
		return nil, nil
	}

	if len(response.KeyPairs) != 1 {
		return nil, fmt.Errorf("Found multiple SSHKeys with Name %q", *e.Name)
	}

	k := response.KeyPairs[0]

	actual := &SSHKey{
		Name:           k.KeyName,
		KeyFingerprint: k.KeyFingerprint,
	}

	// Avoid spurious changes
	if fi.StringValue(actual.KeyFingerprint) == fi.StringValue(e.KeyFingerprint) {
		glog.V(2).Infof("SSH key fingerprints match; assuming public keys match")
		actual.PublicKey = e.PublicKey
	} else {
		glog.V(2).Infof("Computed SSH key fingerprint mismatch: %q %q", fi.StringValue(e.KeyFingerprint), fi.StringValue(actual.KeyFingerprint))
	}

	return actual, nil
}
Beispiel #12
0
func (e *InternetGateway) Find(c *fi.Context) (*InternetGateway, error) {
	cloud := c.Cloud.(*awsup.AWSCloud)

	request := &ec2.DescribeInternetGatewaysInput{}

	shared := fi.BoolValue(e.Shared)
	if shared {
		if fi.StringValue(e.VPC.ID) == "" {
			return nil, fmt.Errorf("VPC ID is required when InternetGateway is shared")
		}

		request.Filters = []*ec2.Filter{awsup.NewEC2Filter("attachment.vpc-id", *e.VPC.ID)}
	} else {
		if e.ID != nil {
			request.InternetGatewayIds = []*string{e.ID}
		} else {
			request.Filters = cloud.BuildFilters(e.Name)
		}
	}

	response, err := cloud.EC2.DescribeInternetGateways(request)
	if err != nil {
		return nil, fmt.Errorf("error listing InternetGateways: %v", err)
	}
	if response == nil || len(response.InternetGateways) == 0 {
		return nil, nil
	}

	if len(response.InternetGateways) != 1 {
		return nil, fmt.Errorf("found multiple InternetGateways matching tags")
	}
	igw := response.InternetGateways[0]
	actual := &InternetGateway{
		ID:   igw.InternetGatewayId,
		Name: findNameTag(igw.Tags),
	}

	glog.V(2).Infof("found matching InternetGateway %q", *actual.ID)

	for _, attachment := range igw.Attachments {
		actual.VPC = &VPC{ID: attachment.VpcId}
	}

	// Prevent spurious comparison failures
	actual.Shared = e.Shared
	if e.ID == nil {
		e.ID = actual.ID
	}

	return actual, nil
}
Beispiel #13
0
func (s *LoadBalancer) CheckChanges(a, e, changes *LoadBalancer) error {
	if a == nil {
		if fi.StringValue(e.Name) == "" {
			return fi.RequiredField("Name")
		}
		if len(e.SecurityGroups) == 0 {
			return fi.RequiredField("SecurityGroups")
		}
		if len(e.Subnets) == 0 {
			return fi.RequiredField("Subnets")
		}
	}
	return nil
}
Beispiel #14
0
func (_ *Secret) Render(c *fi.Context, a, e, changes *Secret) error {
	name := fi.StringValue(e.Name)
	if name == "" {
		return fi.RequiredField("Name")
	}

	secrets := c.SecretStore

	_, _, err := secrets.GetOrCreateSecret(name)
	if err != nil {
		return fmt.Errorf("error creating secret %q: %v", name, err)
	}

	return nil
}
Beispiel #15
0
func (_ *IPAddress) RenderGCE(t *gce.GCEAPITarget, a, e, changes *IPAddress) error {
	addr := &compute.Address{
		Name:    *e.Name,
		Address: fi.StringValue(e.Address),
		Region:  t.Cloud.Region,
	}

	if a == nil {
		glog.Infof("GCE creating address: %q", addr.Name)

		_, err := t.Cloud.Compute.Addresses.Insert(t.Cloud.Project, t.Cloud.Region, addr).Do()
		if err != nil {
			return fmt.Errorf("error creating IPAddress: %v", err)
		}
	} else {
		return fmt.Errorf("Cannot apply changes to IPAddress: %v", changes)
	}

	return nil
}
Beispiel #16
0
func (e *Keypair) Find(c *fi.Context) (*Keypair, error) {
	castore := c.CAStore

	name := fi.StringValue(e.Name)
	if name == "" {
		return nil, nil
	}

	cert, err := castore.FindCert(name)
	if err != nil {
		return nil, err
	}
	if cert == nil {
		return nil, nil
	}

	key, err := castore.FindPrivateKey(name)
	if err != nil {
		return nil, err
	}
	if key == nil {
		return nil, fmt.Errorf("found cert in store, but did not find private key: %q", name)
	}

	var alternateNames []string
	alternateNames = append(alternateNames, cert.Certificate.DNSNames...)
	alternateNames = append(alternateNames, cert.Certificate.EmailAddresses...)
	for _, ip := range cert.Certificate.IPAddresses {
		alternateNames = append(alternateNames, ip.String())
	}
	sort.Strings(alternateNames)

	actual := &Keypair{
		Name:           &name,
		Subject:        pkixNameToString(&cert.Subject),
		AlternateNames: alternateNames,
		Type:           buildTypeDescription(cert.Certificate),
	}

	return actual, nil
}
Beispiel #17
0
func (e *Secret) Find(c *fi.Context) (*Secret, error) {
	secrets := c.SecretStore

	name := fi.StringValue(e.Name)
	if name == "" {
		return nil, nil
	}

	secret, err := secrets.FindSecret(name)
	if err != nil {
		return nil, err
	}
	if secret == nil {
		return nil, nil
	}

	actual := &Secret{
		Name: &name,
	}

	return actual, nil
}
Beispiel #18
0
func (_ *File) RenderLocal(t *local.LocalTarget, a, e, changes *File) error {
	dirMode := os.FileMode(0755)
	fileMode, err := fi.ParseFileMode(fi.StringValue(e.Mode), 0644)
	if err != nil {
		return fmt.Errorf("invalid file mode for %q: %q", e.Path, fi.StringValue(e.Mode))
	}

	if a != nil {
		if e.IfNotExists {
			glog.V(2).Infof("file exists and IfNotExists set; skipping %q", e.Path)
			return nil
		}
	}

	changed := false
	if e.Type == FileType_Symlink {
		if changes.Symlink != nil {
			// This will currently fail if the target already exists.
			// That's probably a good thing for now ... it is hard to know what to do here!
			glog.Infof("Creating symlink %q -> %q", e.Path, *changes.Symlink)
			err := os.Symlink(*changes.Symlink, e.Path)
			if err != nil {
				return fmt.Errorf("error creating symlink %q -> %q: %v", e.Path, *changes.Symlink, err)
			}
			changed = true
		}
	} else if e.Type == FileType_Directory {
		if a == nil {
			parent := filepath.Dir(strings.TrimSuffix(e.Path, "/"))
			err := os.MkdirAll(parent, dirMode)
			if err != nil {
				return fmt.Errorf("error creating parent directories %q: %v", parent, err)
			}

			err = os.MkdirAll(e.Path, fileMode)
			if err != nil {
				return fmt.Errorf("error creating directory %q: %v", e.Path, err)
			}
			changed = true
		}
	} else if e.Type == FileType_File {
		if changes.Contents != nil {
			err = fi.WriteFile(e.Path, e.Contents, fileMode, dirMode)
			if err != nil {
				return fmt.Errorf("error copying file %q: %v", e.Path, err)
			}
			changed = true
		}
	} else {
		return fmt.Errorf("File type=%q not valid/supported", e.Type)
	}

	if changes.Mode != nil {
		modeChanged, err := fi.EnsureFileMode(e.Path, fileMode)
		if err != nil {
			return fmt.Errorf("error changing mode on %q: %v", e.Path, err)
		}
		changed = changed || modeChanged
	}

	if changes.Owner != nil || changes.Group != nil {
		ownerChanged, err := fi.EnsureFileOwner(e.Path, fi.StringValue(e.Owner), fi.StringValue(e.Group))
		if err != nil {
			return fmt.Errorf("error changing owner/group on %q: %v", e.Path, err)
		}
		changed = changed || ownerChanged
	}

	if changed && e.OnChangeExecute != nil {
		args := e.OnChangeExecute
		human := strings.Join(args, " ")

		glog.Infof("Changed; will execute OnChangeExecute command: %q", human)

		cmd := exec.Command(args[0], args[1:]...)
		output, err := cmd.CombinedOutput()
		if err != nil {
			return fmt.Errorf("error executing command %q: %v\nOutput: %s", human, err, output)
		}
	}

	return nil
}
Beispiel #19
0
func (_ *Keypair) Render(c *fi.Context, a, e, changes *Keypair) error {
	name := fi.StringValue(e.Name)
	if name == "" {
		return fi.RequiredField("Name")
	}

	castore := c.CAStore

	template, err := buildCertificateTemplate(e.Type)
	if err != nil {
		return err
	}

	subjectPkix, err := parsePkixName(e.Subject)
	if err != nil {
		return fmt.Errorf("error parsing Subject: %v", err)
	}

	if len(subjectPkix.ToRDNSequence()) == 0 {
		return fmt.Errorf("Subject name was empty for SSL keypair %q", name)
	}

	template.Subject = *subjectPkix

	var alternateNames []string
	alternateNames = append(alternateNames, e.AlternateNames...)

	for _, san := range alternateNames {
		san = strings.TrimSpace(san)
		if san == "" {
			continue
		}
		if ip := net.ParseIP(san); ip != nil {
			template.IPAddresses = append(template.IPAddresses, ip)
		} else {
			template.DNSNames = append(template.DNSNames, san)
		}
	}

	createCertificate := false
	if a == nil {
		createCertificate = true
	} else if changes != nil {
		if changes.AlternateNames != nil {
			createCertificate = true
		} else {
			glog.Warningf("Ignoring changes in key: %v", fi.DebugAsJsonString(changes))
		}
	}

	if createCertificate {
		glog.V(2).Infof("Creating PKI keypair %q", name)

		// TODO: Reuse private key if already exists?
		cert, _, err := castore.CreateKeypair(name, template)
		if err != nil {
			return err
		}

		glog.V(8).Infof("created certificate %v", cert)
	}

	// TODO: Check correct subject / flags

	return nil
}
Beispiel #20
0
func (_ *Service) RenderLocal(t *local.LocalTarget, a, e, changes *Service) error {
	serviceName := e.Name

	action := ""

	if changes.Running != nil && fi.BoolValue(e.ManageState) {
		if fi.BoolValue(e.Running) {
			action = "restart"
		} else {
			action = "stop"
		}
	}

	if changes.Definition != nil {
		servicePath := path.Join(systemdSystemPath, serviceName)
		err := fi.WriteFile(servicePath, fi.NewStringResource(*e.Definition), 0644, 0755)
		if err != nil {
			return fmt.Errorf("error writing systemd service file: %v", err)
		}

		glog.Infof("Reloading systemd configuration")
		cmd := exec.Command("systemctl", "daemon-reload")
		output, err := cmd.CombinedOutput()
		if err != nil {
			return fmt.Errorf("error doing systemd daemon-reload: %v\nOutput: %s", err, output)
		}
	}

	// "SmartRestart" - look at the obvious dependencies in the systemd service, restart if start time older
	if fi.BoolValue(e.ManageState) && fi.BoolValue(e.SmartRestart) {
		definition := fi.StringValue(e.Definition)
		if definition == "" && a != nil {
			definition = fi.StringValue(a.Definition)
		}

		if action == "" && fi.BoolValue(e.Running) && definition != "" {
			dependencies, err := getSystemdDependencies(serviceName, definition)
			if err != nil {
				return err
			}

			var newest time.Time
			for _, dependency := range dependencies {
				stat, err := os.Stat(dependency)
				if err != nil {
					glog.Infof("Ignoring error checking service dependency %q: %v", dependency, err)
					continue
				}
				modTime := stat.ModTime()
				if newest.IsZero() || newest.Before(modTime) {
					newest = modTime
				}
			}

			if !newest.IsZero() {
				properties, err := getSystemdStatus(e.Name)
				if err != nil {
					return err
				}

				startedAt := properties["ExecMainStartTimestamp"]
				if startedAt == "" {
					glog.Warningf("service was running, but did not have ExecMainStartTimestamp: %q", serviceName)
				} else {
					startedAtTime, err := time.Parse("Mon 2006-01-02 15:04:05 MST", startedAt)
					if err != nil {
						return fmt.Errorf("unable to parse service ExecMainStartTimestamp: %q", startedAt)
					}
					if startedAtTime.Before(newest) {
						glog.V(2).Infof("will restart service %q because dependency changed after service start", serviceName)
						action = "restart"
					} else {
						glog.V(2).Infof("will not restart service %q - started after dependencies", serviceName)
					}
				}
			}
		}
	}

	if action != "" && fi.BoolValue(e.ManageState) {
		glog.Infof("Restarting service %q", serviceName)
		cmd := exec.Command("systemctl", action, serviceName)
		output, err := cmd.CombinedOutput()
		if err != nil {
			return fmt.Errorf("error doing systemd %s %s: %v\nOutput: %s", action, serviceName, err, output)
		}
	}

	return nil
}