Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 41 additions & 0 deletions pkg/asset/installconfig/aws/availabilityzones.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package aws

import (
"context"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/pkg/errors"
)

// availabilityZones retrieves a list of availability zones for the given region.
func availabilityZones(ctx context.Context, session *session.Session, region string) ([]string, error) {
client := ec2.New(session, aws.NewConfig().WithRegion(region))
resp, err := client.DescribeAvailabilityZonesWithContext(ctx, &ec2.DescribeAvailabilityZonesInput{
Filters: []*ec2.Filter{
{
Name: aws.String("region-name"),
Values: []*string{aws.String(region)},
},
{
Name: aws.String("state"),
Values: []*string{aws.String("available")},
},
},
})
if err != nil {
return nil, errors.Wrap(err, "fetching availability zones")
}

zones := []string{}
for _, zone := range resp.AvailabilityZones {
zones = append(zones, *zone.ZoneName)
}

if len(zones) == 0 {
return nil, errors.Errorf("no available zones in %s", region)
}

return zones, nil
}
2 changes: 2 additions & 0 deletions pkg/asset/installconfig/aws/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package aws collects AWS-specific configuration.
package aws
65 changes: 65 additions & 0 deletions pkg/asset/installconfig/aws/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package aws

import (
"context"
"sync"

"github.com/aws/aws-sdk-go/aws/session"
"github.com/pkg/errors"
)

// Metadata holds additional metadata for InstallConfig resources that
// does not need to be user-supplied (e.g. because it can be retrieved
// from external APIs).
type Metadata struct {
session *session.Session
availabilityZones []string
region string
mutex sync.Mutex
}

// NewMetadata initializes a new Metadata object.
func NewMetadata(region string) *Metadata {
return &Metadata{region: region}
}

// Session holds an AWS session which can be used for AWS API calls
// during asset generation.
func (m *Metadata) Session(ctx context.Context) (*session.Session, error) {
m.mutex.Lock()
defer m.mutex.Unlock()

return m.unlockedSession(ctx)
}

func (m *Metadata) unlockedSession(ctx context.Context) (*session.Session, error) {
if m.session == nil {
var err error
m.session, err = GetSession()
if err != nil {
return nil, errors.Wrap(err, "creating AWS session")
}
}

return m.session, nil
}

// AvailabilityZones retrieves a list of availability zones for the configured region.
func (m *Metadata) AvailabilityZones(ctx context.Context) ([]string, error) {
m.mutex.Lock()
defer m.mutex.Unlock()

if len(m.availabilityZones) == 0 {
session, err := m.unlockedSession(ctx)
if err != nil {
return nil, err
}

m.availabilityZones, err = availabilityZones(ctx, session, m.region)
if err != nil {
return nil, errors.Wrap(err, "creating AWS session")
}
}

return m.availabilityZones, nil
}
78 changes: 78 additions & 0 deletions pkg/asset/installconfig/aws/platform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package aws

import (
"fmt"
"sort"
"strings"

"github.com/openshift/installer/pkg/types/aws"
"github.com/openshift/installer/pkg/types/aws/validation"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
survey "gopkg.in/AlecAivazis/survey.v1"
)

// Platform collects AWS-specific configuration.
func Platform() (*aws.Platform, error) {
longRegions := make([]string, 0, len(validation.Regions))
shortRegions := make([]string, 0, len(validation.Regions))
for id, location := range validation.Regions {
longRegions = append(longRegions, fmt.Sprintf("%s (%s)", id, location))
shortRegions = append(shortRegions, id)
}
regionTransform := survey.TransformString(func(s string) string {
return strings.SplitN(s, " ", 2)[0]
})

defaultRegion := "us-east-1"
_, ok := validation.Regions[defaultRegion]
if !ok {
panic(fmt.Sprintf("installer bug: invalid default AWS region %q", defaultRegion))
}

ssn, err := GetSession()
if err != nil {
return nil, err
}

defaultRegionPointer := ssn.Config.Region
if defaultRegionPointer != nil && *defaultRegionPointer != "" {
_, ok := validation.Regions[*defaultRegionPointer]
if ok {
defaultRegion = *defaultRegionPointer
} else {
logrus.Warnf("Unrecognized AWS region %q, defaulting to %s", *defaultRegionPointer, defaultRegion)
}
}

sort.Strings(longRegions)
sort.Strings(shortRegions)

var region string
err = survey.Ask([]*survey.Question{
{
Prompt: &survey.Select{
Message: "Region",
Help: "The AWS region to be used for installation.",
Default: fmt.Sprintf("%s (%s)", defaultRegion, validation.Regions[defaultRegion]),
Options: longRegions,
},
Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error {
choice := regionTransform(ans).(string)
i := sort.SearchStrings(shortRegions, choice)
if i == len(shortRegions) || shortRegions[i] != choice {
return errors.Errorf("invalid region %q", choice)
}
return nil
}),
Transform: regionTransform,
},
}, &region)
if err != nil {
return nil, err
}

return &aws.Platform{
Region: region,
}, nil
}
Original file line number Diff line number Diff line change
@@ -1,91 +1,20 @@
// Package aws collects AWS-specific configuration.
package aws

import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"

"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/defaults"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/openshift/installer/pkg/types/aws"
"github.com/openshift/installer/pkg/types/aws/validation"
"github.com/openshift/installer/pkg/version"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
survey "gopkg.in/AlecAivazis/survey.v1"
ini "gopkg.in/ini.v1"
)

// Platform collects AWS-specific configuration.
func Platform() (*aws.Platform, error) {
longRegions := make([]string, 0, len(validation.Regions))
shortRegions := make([]string, 0, len(validation.Regions))
for id, location := range validation.Regions {
longRegions = append(longRegions, fmt.Sprintf("%s (%s)", id, location))
shortRegions = append(shortRegions, id)
}
regionTransform := survey.TransformString(func(s string) string {
return strings.SplitN(s, " ", 2)[0]
})

defaultRegion := "us-east-1"
_, ok := validation.Regions[defaultRegion]
if !ok {
panic(fmt.Sprintf("installer bug: invalid default AWS region %q", defaultRegion))
}

ssn, err := GetSession()
if err != nil {
return nil, err
}

defaultRegionPointer := ssn.Config.Region
if defaultRegionPointer != nil && *defaultRegionPointer != "" {
_, ok := validation.Regions[*defaultRegionPointer]
if ok {
defaultRegion = *defaultRegionPointer
} else {
logrus.Warnf("Unrecognized AWS region %q, defaulting to %s", *defaultRegionPointer, defaultRegion)
}
}

sort.Strings(longRegions)
sort.Strings(shortRegions)

var region string
err = survey.Ask([]*survey.Question{
{
Prompt: &survey.Select{
Message: "Region",
Help: "The AWS region to be used for installation.",
Default: fmt.Sprintf("%s (%s)", defaultRegion, validation.Regions[defaultRegion]),
Options: longRegions,
},
Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error {
choice := regionTransform(ans).(string)
i := sort.SearchStrings(shortRegions, choice)
if i == len(shortRegions) || shortRegions[i] != choice {
return errors.Errorf("invalid region %q", choice)
}
return nil
}),
Transform: regionTransform,
},
}, &region)
if err != nil {
return nil, err
}

return &aws.Platform{
Region: region,
}, nil
}

// GetSession returns an AWS session by checking credentials
// and, if no creds are found, asks for them and stores them on disk in a config file
func GetSession() (*session.Session, error) {
Expand Down
55 changes: 21 additions & 34 deletions pkg/asset/installconfig/installconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/openshift/installer/pkg/asset"
"github.com/openshift/installer/pkg/asset/installconfig/aws"
"github.com/openshift/installer/pkg/types"
"github.com/openshift/installer/pkg/types/conversion"
"github.com/openshift/installer/pkg/types/defaults"
Expand All @@ -23,6 +24,7 @@ const (
type InstallConfig struct {
Config *types.InstallConfig `json:"config"`
File *asset.File `json:"file"`
AWS *aws.Metadata
}

var _ asset.WritableAsset = (*InstallConfig)(nil)
Expand Down Expand Up @@ -75,23 +77,7 @@ func (a *InstallConfig) Generate(parents asset.Parents) error {
a.Config.GCP = platform.GCP
a.Config.BareMetal = platform.BareMetal

if err := a.setDefaults(); err != nil {
return errors.Wrap(err, "failed to set defaults for install config")
}

if err := validation.ValidateInstallConfig(a.Config, openstackvalidation.NewValidValuesFetcher()).ToAggregate(); err != nil {
return errors.Wrap(err, "invalid install config")
}

data, err := yaml.Marshal(a.Config)
Copy link
Contributor

Choose a reason for hiding this comment

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

would like to keep this part in the asset itself.

and finish could take *InstallConfig for better api.

Copy link
Member Author

Choose a reason for hiding this comment

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

would like to keep this part in the asset itself.

We currently marshal in Generate and in Load (presumably to capture any default injection so it doesn't need to get re-injected next time, it was added here). So I think it makes sense to have it in the shared finish helper, but can push it back up into the doubled Generate / Load logic if you prefer. Thoughts?

and finish could take *InstallConfig for better api.

You want func finish(a *InstallConfig, filename string) error instead of func (a *InstallConfig) finish(filename string) error? I don't see a benefit to a function over my current method, but I can reroll to use a function if you like. Just let me know that I'm reading your comment correctly.

Copy link
Contributor

Choose a reason for hiding this comment

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

don't care enough. It's fine as is.

if err != nil {
return errors.Wrap(err, "failed to Marshal InstallConfig")
}
a.File = &asset.File{
Filename: installConfigFilename,
Data: data,
}
return nil
return a.finish("")
}

// Name returns the human-friendly name of the asset.
Expand Down Expand Up @@ -124,37 +110,38 @@ func (a *InstallConfig) Load(f asset.FileFetcher) (found bool, err error) {
a.Config = config

// Upconvert any deprecated fields
if err := a.convert(); err != nil {
if err := conversion.ConvertInstallConfig(a.Config); err != nil {
return false, errors.Wrap(err, "failed to upconvert install config")
}

if err := a.setDefaults(); err != nil {
return false, errors.Wrap(err, "failed to set defaults for install config")
err = a.finish(installConfigFilename)
if err != nil {
return false, err
}
return true, nil
}

func (a *InstallConfig) finish(filename string) error {
defaults.SetInstallConfigDefaults(a.Config)

if a.Config.AWS != nil {
a.AWS = aws.NewMetadata(a.Config.Platform.AWS.Region)
}

if err := validation.ValidateInstallConfig(a.Config, openstackvalidation.NewValidValuesFetcher()).ToAggregate(); err != nil {
return false, errors.Wrapf(err, "invalid %q file", installConfigFilename)
if filename == "" {
return errors.Wrap(err, "invalid install config")
}
return errors.Wrapf(err, "invalid %q file", filename)
}

data, err := yaml.Marshal(a.Config)
if err != nil {
return false, errors.Wrap(err, "failed to Marshal InstallConfig")
return errors.Wrap(err, "failed to Marshal InstallConfig")
}
a.File = &asset.File{
Filename: installConfigFilename,
Data: data,
}

return true, nil
}

func (a *InstallConfig) setDefaults() error {
defaults.SetInstallConfigDefaults(a.Config)
return nil
}

// convert converts possibly older versions of the install config to
// the current version, relocating deprecated fields.
func (a *InstallConfig) convert() error {
return conversion.ConvertInstallConfig(a.Config)
}
Loading