Skip to content
This repository has been archived by the owner on May 22, 2020. It is now read-only.

Handling configurable machine setup/installation #664

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5cd6e74
Rename GCEProviderConfig.Image field to OS
kcoronado Apr 2, 2018
6bc01dd
Replace hardcoded startup script with machine setup configs.
kcoronado Mar 22, 2018
242d193
Remove call to preloaded script in generate-image
kcoronado Mar 27, 2018
9b9a8b9
Create/mount configmap for machine setup configs
kcoronado Mar 28, 2018
bd5d0ec
fixup 6bc01dd Remove nil check for ConfigWatch
kcoronado Apr 4, 2018
a2782a1
fixup 6bc01dd fix metadata method parameters
kcoronado Apr 5, 2018
dfc5f96
fixup 6bc01dd must use full image path in configs
kcoronado Apr 5, 2018
9c5614b
fixup 6bc01dd fix config_types.go
kcoronado Apr 5, 2018
88ceb5d
fixup 6bc01dd fix config_types_test.go
kcoronado Apr 5, 2018
c79b5a0
fixup 9b9a8b9 fix config_types_test.go
kcoronado Apr 5, 2018
7e00e5b
fixup 6bc01dd fix create flag and default value
kcoronado Apr 5, 2018
1cc6a4d
fixup 6bc01dd fix spacing in configs file
kcoronado Apr 5, 2018
078e7e8
fixup 6bc01dd add TODO
kcoronado Apr 5, 2018
5c450a0
fixup 6bc01dd fix env vars template bug
kcoronado Apr 5, 2018
e1d2b28
fixup 6bc01dd roles must be exact match
kcoronado Apr 10, 2018
6eea28e
fixup 6bc01dd move ARCH env var to scripts
kcoronado Apr 10, 2018
66a419f
fixup 6bc01dd fix role matching check
kcoronado Apr 10, 2018
e829e94
fixup 6bc01dd rename gce-machine-controller flag
kcoronado Apr 12, 2018
c124da1
fixup 6bc01dd add comments/documentation
kcoronado Apr 13, 2018
c1c88ef
fixup 6bc01dd return error if >1 config match
kcoronado Apr 13, 2018
1cfe66b
fixup 9b9a8b9 add TODO for improving configmap visibility
kcoronado Apr 18, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cluster-api/cloud/google/cmd/gce-machine-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import (
)

var (
kubeadmToken = pflag.String("token", "", "Kubeadm token to use to join new machines")
kubeadmToken = pflag.String("token", "", "Kubeadm token to use to join new machines")
machineSetupConfigsPath = pflag.String("config", "", "path to machine setup configs file")
)

func init() {
Expand All @@ -54,7 +55,7 @@ func main() {
glog.Fatalf("Could not create client for talking to the apiserver: %v", err)
}

actuator, err := google.NewMachineActuator(*kubeadmToken, client.ClusterV1alpha1().Machines(corev1.NamespaceDefault))
actuator, err := google.NewMachineActuator(*kubeadmToken, client.ClusterV1alpha1().Machines(corev1.NamespaceDefault), *machineSetupConfigsPath)
if err != nil {
glog.Fatalf("Could not create Google machine actuator: %v", err)
}
Expand Down
31 changes: 12 additions & 19 deletions cluster-api/cloud/google/cmd/generate-image/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,12 @@ import (

"github.com/golang/glog"
"github.com/spf13/cobra"
"k8s.io/kube-deploy/cluster-api/cloud/google"
"io/ioutil"
"os"
)

type options struct {
version string
role string
dockerImages []string
script string
}

var opts options
Expand All @@ -36,37 +35,31 @@ var generateCmd = &cobra.Command{
Use: "generate_image",
Short: "Outputs a script to generate a preloaded image",
Run: func(cmd *cobra.Command, args []string) {
if opts.script == "" {
glog.Error("Please provide a startup script.")
cmd.Help()
os.Exit(1)
}

if err := runGenerate(opts); err != nil {
glog.Exit(err)
}
},
}

func init() {
generateCmd.Flags().StringVar(&opts.version, "version", "1.7.3", "The version of kubernetes to install")
generateCmd.Flags().StringVar(&opts.role, "role", "master", "The role of the machine (master or node)")
generateCmd.Flags().StringArrayVar(&opts.dockerImages, "extra-docker-images", []string{}, "extra docker images to preload")
generateCmd.Flags().StringVar(&opts.script, "script", "", "The path to the machine's startup script")
}

func runGenerate(o options) error {
var script string
var err error
switch o.role {
case "master":
script, err = google.PreloadMasterScript(o.version, o.dockerImages)
case "node":
script, err = google.PreloadMasterScript(o.version, o.dockerImages)
default:
return fmt.Errorf("unrecognized role: %q", o.role)
}

bytes, err := ioutil.ReadFile(o.script)
if err != nil {
return err
}

// just print the script for now
// TODO actually start a VM, let it run the script, stop the VM, then create the image
fmt.Println(script)
fmt.Println(string(bytes))
return nil
}

Expand Down
6 changes: 6 additions & 0 deletions cluster-api/cloud/google/config/configtemplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ spec:
mountPath: /etc/credentials
- name: sshkeys
mountPath: /etc/sshkeys
- name: machine-setup
mountPath: /etc/machinesetup
env:
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /etc/credentials/service-account.json
Expand All @@ -147,6 +149,7 @@ spec:
args:
- --kubeconfig=/etc/kubernetes/admin.conf
- --token={{ .Token }}
- --config=/etc/machinesetup/machine_setup_configs.yaml
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call it this something other than config please. There are way too many things named config. Be specific here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I named it machinesetup to be consistent with the flag in create.go.

resources:
requests:
cpu: 100m
Expand All @@ -171,6 +174,9 @@ spec:
- name: credentials
secret:
secretName: machine-controller-credential
- name: machine-setup
configMap:
name: machine-setup
---
apiVersion: apps/v1beta1
kind: StatefulSet
Expand Down
2 changes: 1 addition & 1 deletion cluster-api/cloud/google/gceproviderconfig/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ type GCEProviderConfig struct {
Project string `json:"project"`
Zone string `json:"zone"`
MachineType string `json:"machineType"`
Image string `json:"image"`
OS string `json:"os"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this OS image or OS name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be the name, it gets mapped to the image in the machine setup configs. I added a comment.

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ type GCEProviderConfig struct {
Project string `json:"project"`
Zone string `json:"zone"`
MachineType string `json:"machineType"`
Image string `json:"image"`
OS string `json:"os"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

}
131 changes: 76 additions & 55 deletions cluster-api/cloud/google/machineactuator.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,12 @@ import (

"regexp"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
gceconfig "k8s.io/kube-deploy/cluster-api/cloud/google/gceproviderconfig"
gceconfigv1 "k8s.io/kube-deploy/cluster-api/cloud/google/gceproviderconfig/v1alpha1"
"k8s.io/kube-deploy/cluster-api/cloud/google/machinesetup"
apierrors "k8s.io/kube-deploy/cluster-api/errors"
clusterv1 "k8s.io/kube-deploy/cluster-api/pkg/apis/cluster/v1alpha1"
client "k8s.io/kube-deploy/cluster-api/pkg/client/clientset_generated/clientset/typed/cluster/v1alpha1"
Expand All @@ -52,6 +56,8 @@ const (

UIDLabelKey = "machine-crd-uid"
BootstrapLabelKey = "boostrap"

MachineSetupConfigsFilename = "machine_setup_configs.yaml"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment about what this file contains and in what format.

)

type SshCreds struct {
Expand All @@ -66,14 +72,15 @@ type GCEClient struct {
kubeadmToken string
sshCreds SshCreds
machineClient client.MachineInterface
configWatch *machinesetup.ConfigWatch
}

const (
gceTimeout = time.Minute * 10
gceWaitSleep = time.Second * 5
)

func NewMachineActuator(kubeadmToken string, machineClient client.MachineInterface) (*GCEClient, error) {
func NewMachineActuator(kubeadmToken string, machineClient client.MachineInterface, configListPath string) (*GCEClient, error) {
// The default GCP client expects the environment variable
// GOOGLE_APPLICATION_CREDENTIALS to point to a file with service credentials.
client, err := google.DefaultClient(context.TODO(), compute.ComputeScope)
Expand Down Expand Up @@ -104,6 +111,15 @@ func NewMachineActuator(kubeadmToken string, machineClient client.MachineInterfa
}
}

// TODO: get rid of empty string check when we switch to the new bootstrapping method.
var configWatch *machinesetup.ConfigWatch
if configListPath != "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this check is necessary. machinesetup.NewConfigWatch should return an error if this is not a valid path to a file and this function should error-out approrpiately.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create is the only command that has to pass a config list path, and then add and delete pass an empty string to the method. I put this check here to handle the empty string from add and delete, and create has a check to prevent an empty string. NewConfigWatch() checks that the path is valid. I can move the empty string check to NewConfigWatch() so it just returns nil (but no error) if it gets an empty string if that makes more sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. Can we add TODOs to remove this when we switch to the new bootstrapping method. In that setup, we will be running a full-blown controller that will always need a config path.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So would add/delete also need to pass the config path when we switch over (and I should add TODOs there too)?

configWatch, err = machinesetup.NewConfigWatch(configListPath)
if err != nil {
glog.Errorf("Error creating config watch: %v", err)
}
}

return &GCEClient{
service: service,
scheme: scheme,
Expand All @@ -114,10 +130,11 @@ func NewMachineActuator(kubeadmToken string, machineClient client.MachineInterfa
user: user,
},
machineClient: machineClient,
configWatch: configWatch,
}, nil
}

func (gce *GCEClient) CreateMachineController(cluster *clusterv1.Cluster, initialMachines []*clusterv1.Machine) error {
func (gce *GCEClient) CreateMachineController(cluster *clusterv1.Cluster, initialMachines []*clusterv1.Machine, clientSet kubernetes.Clientset) error {
if err := gce.CreateMachineControllerServiceAccount(cluster, initialMachines); err != nil {
return err
}
Expand All @@ -131,6 +148,26 @@ func (gce *GCEClient) CreateMachineController(cluster *clusterv1.Cluster, initia
return err
}

// Create the configmap so the machine setup configs can be mounted into the node.
machineSetupConfigs, err := gce.configWatch.ValidConfigs()
if err != nil {
return err
}
yaml, err := machineSetupConfigs.GetYaml()
if err != nil {
return err
}
configMap := corev1.ConfigMap{
ObjectMeta: v1.ObjectMeta{Name: "machine-setup"},
Data: map[string]string{
MachineSetupConfigsFilename: yaml,
},
}
configMaps := clientSet.CoreV1().ConfigMaps(corev1.NamespaceDefault)
if _, err := configMaps.Create(&configMap); err != nil {
return err
}

if err := CreateApiServerAndController(gce.kubeadmToken); err != nil {
return err
}
Expand All @@ -153,22 +190,32 @@ func (gce *GCEClient) Create(cluster *clusterv1.Cluster, machine *clusterv1.Mach
return errors.New("invalid master configuration: missing Machine.Spec.Versions.Kubelet")
}

image, preloaded := gce.getImage(machine, config)
machineSetupConfigs, err := gce.configWatch.ValidConfigs()
if err != nil {
return err
}
configParams := &machinesetup.ConfigParams{
OS: config.OS,
Roles: machine.Spec.Roles,
Versions: machine.Spec.Versions,
}
image, err := machineSetupConfigs.GetImage(configParams)
if err != nil {
return err
}
imagePath := gce.getImagePath(image)

machineSetupMetadata, err := machineSetupConfigs.GetMetadata(configParams)
if err != nil {
return err
}
if util.IsMaster(machine) {
if machine.Spec.Versions.ControlPlane == "" {
return gce.handleMachineError(machine, apierrors.InvalidMachineConfiguration(
"invalid master configuration: missing Machine.Spec.Versions.ControlPlane"))
}
var err error
metadata, err = masterMetadata(
templateParams{
Token: gce.kubeadmToken,
Cluster: cluster,
Machine: machine,
Preloaded: preloaded,
},
)
metadata, err = masterMetadata(gce.kubeadmToken, cluster, machine, config.Project, &machineSetupMetadata)
if err != nil {
return err
}
Expand All @@ -177,14 +224,7 @@ func (gce *GCEClient) Create(cluster *clusterv1.Cluster, machine *clusterv1.Mach
return errors.New("invalid cluster state: cannot create a Kubernetes node without an API endpoint")
}
var err error
metadata, err = nodeMetadata(
templateParams{
Token: gce.kubeadmToken,
Cluster: cluster,
Machine: machine,
Preloaded: preloaded,
},
)
metadata, err = nodeMetadata(gce.kubeadmToken, cluster, machine, &machineSetupMetadata)
if err != nil {
return err
}
Expand All @@ -207,13 +247,7 @@ func (gce *GCEClient) Create(cluster *clusterv1.Cluster, machine *clusterv1.Mach
name := machine.ObjectMeta.Name
project := config.Project
zone := config.Zone
diskSize := int64(10)

// Our preloaded image already has a lot stored on it, so increase the
// disk size to have more free working space.
if preloaded {
diskSize = 30
}
diskSize := int64(30)

if instance == nil {
labels := map[string]string{
Expand Down Expand Up @@ -242,7 +276,7 @@ func (gce *GCEClient) Create(cluster *clusterv1.Cluster, machine *clusterv1.Mach
AutoDelete: true,
Boot: true,
InitializeParams: &compute.AttachedDiskInitializeParams{
SourceImage: image,
SourceImage: imagePath,
DiskSizeGb: diskSize,
},
},
Expand Down Expand Up @@ -653,42 +687,29 @@ func (gce *GCEClient) handleMachineError(machine *clusterv1.Machine, err *apierr
return err
}

func (gce *GCEClient) getImage(machine *clusterv1.Machine, config *gceconfig.GCEProviderConfig) (image string, isPreloaded bool) {
func (gce *GCEClient) getImagePath(img string) (imagePath string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great method for testing with unit tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but it makes a call to the GCE API to check that the image is there and I was having trouble figuring out how to mock it (otherwise I would have added the test when I refactored this method in a previous PR). Rob is working on a PR (#688) to set up a fake client so I can write a unit test for this method once that gets merged.

defaultImg := "projects/ubuntu-os-cloud/global/images/family/ubuntu-1710"
project := config.Project
img := config.Image

// A full image path must match the regex format. If it doesn't, we'll assume it's just the image name and try to get it.
// If that doesn't work, we will fall back to a default base image.
// A full image path must match the regex format. If it doesn't, we will fall back to a default base image.
matches := regexp.MustCompile("projects/(.+)/global/images/(family/)*(.+)").FindStringSubmatch(img)
if matches == nil {
// Only the image name was specified in config, so check if it is preloaded in the project specified in config.
fullPath := fmt.Sprintf("projects/%s/global/images/%s", project, img)
if _, err := gce.service.Images.Get(project, img).Do(); err == nil {
return fullPath, false
if matches != nil {
// Check to see if the image exists in the given path. The presence of "family" in the path dictates which API call we need to make.
project, family, name := matches[1], matches[2], matches[3]
var err error
if family == "" {
_, err = gce.service.Images.Get(project, name).Do()
} else {
_, err = gce.service.Images.GetFromFamily(project, name).Do()
}

// Otherwise, fall back to the non-preloaded base image.
glog.Infof("Could not find image at %s. Defaulting to %s.", fullPath, defaultImg)
return defaultImg, false
}

// Check to see if the image exists in the given path. The presence of "family" in the path dictates which API call we need to make.
project, family, name := matches[1], matches[2], matches[3]
var err error
if family == "" {
_, err = gce.service.Images.Get(project, name).Do()
} else {
_, err = gce.service.Images.GetFromFamily(project, name).Do()
}

if err == nil {
return img, false
if err == nil {
return img
}
}

// Otherwise, fall back to the non-preloaded base image.
// Otherwise, fall back to the base image.
glog.Infof("Could not find image at %s. Defaulting to %s.", img, defaultImg)
return defaultImg, false
return defaultImg
}

// Just a temporary hack to grab a single range from the config.
Expand Down
Loading