diff --git a/cmd/openshift-install/gather.go b/cmd/openshift-install/gather.go index 921794c79c4..86fe4f9d36f 100644 --- a/cmd/openshift-install/gather.go +++ b/cmd/openshift-install/gather.go @@ -1,9 +1,7 @@ package main import ( - "encoding/json" "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -11,11 +9,13 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/openshift/installer/pkg/asset/installconfig" assetstore "github.com/openshift/installer/pkg/asset/store" "github.com/openshift/installer/pkg/terraform" + gatheraws "github.com/openshift/installer/pkg/terraform/gather/aws" + gatherlibvirt "github.com/openshift/installer/pkg/terraform/gather/libvirt" + gatheropenstack "github.com/openshift/installer/pkg/terraform/gather/openstack" "github.com/openshift/installer/pkg/types" awstypes "github.com/openshift/installer/pkg/types/aws" libvirttypes "github.com/openshift/installer/pkg/types/libvirt" @@ -85,16 +85,10 @@ func runGatherBootstrapCmd(directory string) error { return errors.Wrapf(err, "failed to fetch %s", config.Name()) } - sfRaw, err := ioutil.ReadFile(tfStateFilePath) + tfstate, err := terraform.ReadState(tfStateFilePath) if err != nil { - return errors.Wrapf(err, "failed to read %q", tfStateFilePath) + return errors.Wrapf(err, "failed to read state from %q", tfStateFilePath) } - - var tfstate terraformState - if err := json.Unmarshal(sfRaw, &tfstate); err != nil { - return errors.Wrapf(err, "failed to unmarshal %q", tfStateFilePath) - } - bootstrap, masters, err := extractHostAddresses(config.Config, tfstate) if err != nil { if err2, ok := err.(errUnSupportedGatherPlatform); ok { @@ -117,68 +111,34 @@ func logGatherBootstrap(bootstrap string, masters []string) { logrus.Infof("scp core@%s:~/log-bundle.tar.gz .", bootstrap) } -func extractHostAddresses(config *types.InstallConfig, tfstate terraformState) (bootstrap string, masters []string, err error) { - mcount := *config.ControlPlane.Replicas +func extractHostAddresses(config *types.InstallConfig, tfstate *terraform.State) (bootstrap string, masters []string, err error) { switch config.Platform.Name() { case awstypes.Name: - bm := tfstate.Modules["root/bootstrap"] - bootstrap, _, err = unstructured.NestedString(bm.Resources["aws_instance.bootstrap"], "primary", "attributes", "public_ip") + bootstrap, err = gatheraws.BootstrapIP(tfstate) if err != nil { - return bootstrap, masters, errors.Wrapf(err, "failed to get bootstrap host addresses") + return bootstrap, masters, err } - - mm := tfstate.Modules["root/masters"] - for idx := int64(0); idx < mcount; idx++ { - r := fmt.Sprintf("aws_instance.master.%d", idx) - if mcount == 1 { - r = "aws_instance.master" - } - var master string - master, _, err = unstructured.NestedString(mm.Resources[r], "primary", "attributes", "private_ip") - if err != nil { - return bootstrap, masters, errors.Wrapf(err, "failed to get master host addresses") - } - masters = append(masters, master) + masters, err = gatheraws.ControlPlaneIPs(tfstate) + if err != nil { + logrus.Error(err) } case libvirttypes.Name: - bm := tfstate.Modules["root/bootstrap"] - bootstrap, _, err = unstructured.NestedString(bm.Resources["libvirt_domain.bootstrap"], "primary", "attributes", "network_interface.0.hostname") + bootstrap, err = gatherlibvirt.BootstrapIP(tfstate) if err != nil { - return bootstrap, masters, errors.Wrapf(err, "failed to get bootstrap host addresses") + return bootstrap, masters, err } - - rm := tfstate.Modules["root"] - for idx := int64(0); idx < mcount; idx++ { - r := fmt.Sprintf("libvirt_domain.master.%d", idx) - if mcount == 1 { - r = "libvirt_domain.master" - } - var master string - master, _, err = unstructured.NestedString(rm.Resources[r], "primary", "attributes", "network_interface.0.hostname") - if err != nil { - return bootstrap, masters, errors.Wrapf(err, "failed to get master host addresses") - } - masters = append(masters, master) + masters, err = gatherlibvirt.ControlPlaneIPs(tfstate) + if err != nil { + logrus.Error(err) } case openstacktypes.Name: - bm := tfstate.Modules["root/bootstrap"] - bootstrap, _, err = unstructured.NestedString(bm.Resources["openstack_compute_instance_v2.bootstrap"], "primary", "attributes", "access_ip_v4") + bootstrap, err = gatheropenstack.BootstrapIP(tfstate) if err != nil { - return bootstrap, masters, errors.Wrapf(err, "failed to get bootstrap host addresses") + return bootstrap, masters, err } - - mm := tfstate.Modules["root/masters"] - for idx := int64(0); idx < mcount; idx++ { - r := fmt.Sprintf("openstack_compute_instance_v2.master_conf.%d", idx) - if mcount == 1 { - r = "openstack_compute_instance_v2.master_conf" - } - var master string - master, _, err = unstructured.NestedString(mm.Resources[r], "primary", "attributes", "access_ip_v4") - if err != nil { - return bootstrap, masters, errors.Wrapf(err, "failed to get master host addresses") - } - masters = append(masters, master) + masters, err = gatheropenstack.ControlPlaneIPs(tfstate) + if err != nil { + logrus.Error(err) } default: return "", nil, errUnSupportedGatherPlatform{Message: fmt.Sprintf("Cannot fetch the bootstrap and control plane host addresses from state file for %s platform", config.Platform.Name())} @@ -186,36 +146,6 @@ func extractHostAddresses(config *types.InstallConfig, tfstate terraformState) ( return bootstrap, masters, nil } -type terraformState struct { - Modules map[string]terraformStateModule -} - -type terraformStateModule struct { - Resources map[string]map[string]interface{} `json:"resources"` -} - -func (tfs *terraformState) UnmarshalJSON(raw []byte) error { - var transform struct { - Modules []struct { - Path []string `json:"path"` - terraformStateModule - } `json:"modules"` - } - if err := json.Unmarshal(raw, &transform); err != nil { - return err - } - if tfs == nil { - tfs = &terraformState{} - } - if tfs.Modules == nil { - tfs.Modules = make(map[string]terraformStateModule) - } - for _, m := range transform.Modules { - tfs.Modules[strings.Join(m.Path, "/")] = terraformStateModule{Resources: m.Resources} - } - return nil -} - type errUnSupportedGatherPlatform struct { Message string } diff --git a/pkg/terraform/exec/state.go b/pkg/terraform/exec/state.go new file mode 100644 index 00000000000..71a84f8bf6c --- /dev/null +++ b/pkg/terraform/exec/state.go @@ -0,0 +1,32 @@ +package exec + +import ( + "bytes" + "os" + + "github.com/hashicorp/terraform/states/statefile" + "github.com/pkg/errors" +) + +// ReadState reads the terraform state from file and returns the contents in bytes +// It returns an error if reading the state was unsuccessful +// ReadState utilizes the terraform's internal wiring to upconvert versions of terraform state to return +// the state it currently recognizes. +func ReadState(file string) ([]byte, error) { + f, err := os.Open(file) + if err != nil { + return nil, errors.Wrapf(err, "failed to open %q", file) + } + defer f.Close() + + sf, err := statefile.Read(f) + if err != nil { + return nil, errors.Wrapf(err, "failed to read statefile from %q", file) + } + + out := bytes.Buffer{} + if err := statefile.Write(sf, &out); err != nil { + return nil, errors.Wrapf(err, "failed to write statefile") + } + return out.Bytes(), nil +} diff --git a/pkg/terraform/gather/aws/ip.go b/pkg/terraform/gather/aws/ip.go new file mode 100644 index 00000000000..4ff5c9f8e91 --- /dev/null +++ b/pkg/terraform/gather/aws/ip.go @@ -0,0 +1,45 @@ +// Package aws contains utilities that help gather AWS specific +// information from terraform state. +package aws + +import ( + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + + "github.com/openshift/installer/pkg/terraform" +) + +// BootstrapIP returns the ip address for bootstrap host. +func BootstrapIP(tfs *terraform.State) (string, error) { + br, err := terraform.LookupResource(tfs, "module.bootstrap", "aws_instance", "bootstrap") + if err != nil { + return "", errors.Wrap(err, "failed to lookup bootstrap") + } + if len(br.Instances) == 0 { + return "", errors.New("no bootstrap instance found") + } + bootstrap, _, err := unstructured.NestedString(br.Instances[0].Attributes, "public_ip") + if err != nil { + return "", errors.New("no public_ip found for bootstrap") + } + return bootstrap, nil +} + +// ControlPlaneIPs returns the ip addresses for control plane hosts. +func ControlPlaneIPs(tfs *terraform.State) ([]string, error) { + mrs, err := terraform.LookupResource(tfs, "module.masters", "aws_instance", "master") + if err != nil { + return nil, errors.Wrap(err, "failed to lookup masters") + } + var errs []error + var masters []string + for idx, inst := range mrs.Instances { + master, _, err := unstructured.NestedString(inst.Attributes, "private_ip") + if err != nil { + errs = append(errs, errors.Wrapf(err, "no private_ip for master.%d", idx)) + } + masters = append(masters, master) + } + return masters, utilerrors.NewAggregate(errs) +} diff --git a/pkg/terraform/gather/libvirt/ip.go b/pkg/terraform/gather/libvirt/ip.go new file mode 100644 index 00000000000..417cc615034 --- /dev/null +++ b/pkg/terraform/gather/libvirt/ip.go @@ -0,0 +1,60 @@ +// Package libvirt contains utilities that help gather Libvirt specific +// information from terraform state. +package libvirt + +import ( + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + + "github.com/openshift/installer/pkg/terraform" +) + +// BootstrapIP returns the ip address for bootstrap host. +func BootstrapIP(tfs *terraform.State) (string, error) { + br, err := terraform.LookupResource(tfs, "module.bootstrap", "libvirt_domain", "bootstrap") + if err != nil { + return "", errors.Wrap(err, "failed to lookup bootstrap") + } + if len(br.Instances) == 0 { + return "", errors.New("no bootstrap instance found") + } + bootstrap, err := hostnameForDomain(br.Instances[0].Attributes) + if err != nil { + return "", errors.Wrap(err, "failed to lookup hostname") + } + return bootstrap, nil +} + +// ControlPlaneIPs returns the ip addresses for control plane hosts. +func ControlPlaneIPs(tfs *terraform.State) ([]string, error) { + mrs, err := terraform.LookupResource(tfs, "", "libvirt_domain", "master") + if err != nil { + return nil, errors.Wrap(err, "failed to lookup masters") + } + var errs []error + var masters []string + for idx, inst := range mrs.Instances { + master, err := hostnameForDomain(inst.Attributes) + if err != nil { + errs = append(errs, errors.Wrapf(err, "failed to lookup hostname for master.%d", idx)) + } + masters = append(masters, master) + } + return masters, utilerrors.NewAggregate(errs) +} + +func hostnameForDomain(attr map[string]interface{}) (string, error) { + nics, _, err := unstructured.NestedSlice(attr, "network_interface") + if err != nil { + return "", errors.Wrap(err, "failed to lookup network_interface") + } + if len(nics) == 0 { + return "", errors.New("no network_interface found") + } + hostname, _, err := unstructured.NestedString(nics[0].(map[string]interface{}), "hostname") + if err != nil { + return "", errors.New("no hostname found") + } + return hostname, nil +} diff --git a/pkg/terraform/gather/openstack/ip.go b/pkg/terraform/gather/openstack/ip.go new file mode 100644 index 00000000000..f1c8a9e0925 --- /dev/null +++ b/pkg/terraform/gather/openstack/ip.go @@ -0,0 +1,45 @@ +// Package openstack contains utilities that help gather Openstack specific +// information from terraform state. +package openstack + +import ( + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + + "github.com/openshift/installer/pkg/terraform" +) + +// BootstrapIP returns the ip address for bootstrap host. +func BootstrapIP(tfs *terraform.State) (string, error) { + br, err := terraform.LookupResource(tfs, "module.bootstrap", "openstack_compute_instance_v2", "bootstrap") + if err != nil { + return "", errors.Wrap(err, "failed to lookup bootstrap") + } + if len(br.Instances) == 0 { + return "", errors.New("no bootstrap instance found") + } + bootstrap, _, err := unstructured.NestedString(br.Instances[0].Attributes, "access_ip_v4") + if err != nil { + return "", errors.New("no public_ip found for bootstrap") + } + return bootstrap, nil +} + +// ControlPlaneIPs returns the ip addresses for control plane hosts. +func ControlPlaneIPs(tfs *terraform.State) ([]string, error) { + mrs, err := terraform.LookupResource(tfs, "module.masters", "openstack_compute_instance_v2", "master_conf") + if err != nil { + return nil, errors.Wrap(err, "failed to lookup masters") + } + var errs []error + var masters []string + for idx, inst := range mrs.Instances { + master, _, err := unstructured.NestedString(inst.Attributes, "access_ip_v4") + if err != nil { + errs = append(errs, errors.Wrapf(err, "no access_ip_v4 for master_conf.%d", idx)) + } + masters = append(masters, master) + } + return masters, utilerrors.NewAggregate(errs) +} diff --git a/pkg/terraform/state.go b/pkg/terraform/state.go new file mode 100644 index 00000000000..4da91115ee0 --- /dev/null +++ b/pkg/terraform/state.go @@ -0,0 +1,61 @@ +package terraform + +import ( + "encoding/json" + + "github.com/pkg/errors" + + tfexec "github.com/openshift/installer/pkg/terraform/exec" +) + +// State in local sparse representation of terraform state that includes +// the fields important to installer. +type State struct { + Resources []StateResource `json:"resources"` +} + +// StateResource is local sparse representation of terraform state resource that includes +// the fields most important to installer. +type StateResource struct { + Module string `json:"module"` + Name string `json:"name"` + Type string `json:"type"` + Instances []StateResourceInstance `json:"instances"` +} + +// StateResourceInstance is an instance of terraform state resource. +type StateResourceInstance struct { + Attributes map[string]interface{} `json:"attributes"` +} + +// ErrResourceNotFound is an error that instructs that requested resource was not found. +var ErrResourceNotFound = errors.New("resource not found") + +// LookupResource finds a resource for a given module, type and name from the state. +// If module is "root", it is treated as "" +// If no resource is found for the triplet, ErrResourceNotFound error is returned. +func LookupResource(state *State, module, t, name string) (*StateResource, error) { + if module == "root" { + module = "" + } + for idx, r := range state.Resources { + if module == r.Module && t == r.Type && name == r.Name { + return &state.Resources[idx], nil + } + } + return nil, ErrResourceNotFound +} + +// ReadState returns that terraform state from the file. +func ReadState(file string) (*State, error) { + sfRaw, err := tfexec.ReadState(file) + if err != nil { + return nil, errors.Wrapf(err, "failed to read %q", file) + } + + var tfstate State + if err := json.Unmarshal(sfRaw, &tfstate); err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal %q", file) + } + return &tfstate, nil +}