Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pkg/asset: remove asset stocks. store asset state in asset itself. #344

Merged
merged 3 commits into from
Oct 5, 2018
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
48 changes: 26 additions & 22 deletions cmd/openshift-install/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import (
"gopkg.in/alecthomas/kingpin.v2"

"github.com/openshift/installer/pkg/asset"
"github.com/openshift/installer/pkg/asset/stock"
"github.com/openshift/installer/pkg/asset/cluster"
"github.com/openshift/installer/pkg/asset/ignition/bootstrap"
"github.com/openshift/installer/pkg/asset/ignition/machine"
"github.com/openshift/installer/pkg/asset/installconfig"
"github.com/openshift/installer/pkg/asset/kubeconfig"
"github.com/openshift/installer/pkg/asset/manifests"
"github.com/openshift/installer/pkg/asset/metadata"
"github.com/openshift/installer/pkg/destroy"
_ "github.com/openshift/installer/pkg/destroy/libvirt"
"github.com/openshift/installer/pkg/terraform"
Expand Down Expand Up @@ -58,29 +64,27 @@ func main() {
return
}

assetStock := stock.EstablishStock()

var targetAssets []asset.Asset
var targetAssets []asset.WritableAsset
switch command {
case installConfigCommand.FullCommand():
targetAssets = []asset.Asset{assetStock.InstallConfig()}
targetAssets = []asset.WritableAsset{&installconfig.InstallConfig{}}
case ignitionConfigsCommand.FullCommand():
targetAssets = []asset.Asset{
assetStock.BootstrapIgnition(),
assetStock.MasterIgnition(),
assetStock.WorkerIgnition(),
targetAssets = []asset.WritableAsset{
&bootstrap.Bootstrap{},
&machine.Master{},
&machine.Worker{},
}
case manifestsCommand.FullCommand():
targetAssets = []asset.Asset{
assetStock.Manifests(),
assetStock.Tectonic(),
targetAssets = []asset.WritableAsset{
&manifests.Manifests{},
&manifests.Tectonic{},
}
case clusterCommand.FullCommand():
targetAssets = []asset.Asset{
assetStock.TFVars(),
assetStock.KubeconfigAdmin(),
assetStock.Cluster(),
assetStock.Metadata(),
targetAssets = []asset.WritableAsset{
&cluster.TerraformVariables{},
&kubeconfig.Admin{},
&cluster.Cluster{},
&metadata.Metadata{},
}
}

Expand All @@ -90,14 +94,14 @@ func main() {
manifestsCommand.FullCommand(),
clusterCommand.FullCommand():
assetStore := &asset.StoreImpl{}
for _, asset := range targetAssets {
st, err := assetStore.Fetch(asset)
for _, a := range targetAssets {
err := assetStore.Fetch(a)
if err != nil {
logrus.Fatalf("Failed to generate %s: %v", asset.Name(), err)
logrus.Fatalf("Failed to generate %s: %v", a.Name(), err)
}

if err := st.PersistToFile(*dirFlag); err != nil {
logrus.Fatalf("Failed to write asset (%s) to disk: %v", asset.Name(), err)
if err := asset.PersistToFile(a, *dirFlag); err != nil {
logrus.Fatalf("Failed to write asset (%s) to disk: %v", a.Name(), err)
}
}
case destroyCommand.FullCommand():
Expand Down
40 changes: 16 additions & 24 deletions docs/design/assetgeneration.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,6 @@ The installer is also able to read assets from disk if they have been provided b

Each asset is individually responsible for declaring its dependencies. Each asset is also responsible resolving conflicts when combining its input from disk and its state from a previous run. The installer ensures all the dependencies for an asset is generated and provides the asset with latest state to generate its own output.

## State

State is an internal state of the installer. It stores the contents of all the assets that have been generated.

An example of the State:

```go
// State is map of assets
type State struct {
Root string // hash for state object.

Objects map[string]AssetState
}

// AssetState is the state of an Asset.
type AssetState struct {
Parents maps[string]string // hashes for our each parent.
Contents []Content{
Name string // the path on disk for this content.
Data []byte
}
}
```

## Asset

An asset is the generic representation of work-item for installer that needs to be generated. Each asset defines all the other assets that are required for it to generate itself as dependencies.
Expand Down Expand Up @@ -63,6 +39,22 @@ type Asset interface{
}
```

## Writable Asset
abhinavdahiya marked this conversation as resolved.
Show resolved Hide resolved

A writable asset is an asset that generates files to write to disk. These files could be for the user to consume as output from installer targets, such as install-config.yml from the InstallConfig asset. Or these files could be used internally by the installer, such as the cert/key files generated by TLS assets.

```go
type WritableAsset interface{
Asset
Files() []File
}

type File struct {
Filename string
Data []byte
}
```

## Target generation

The installer uses depth-first traversal on the dependency graph, starting at the target nodes, generating all the dependencies of the asset before generating the asset itself. After all the target assets have been generated, the installer outputs the contents of the components of the targets to disk.
Expand Down
43 changes: 41 additions & 2 deletions pkg/asset/asset.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,52 @@
package asset

import (
"io/ioutil"
"os"
"path/filepath"

"github.com/pkg/errors"
)

// Asset used to install OpenShift.
type Asset interface {
// Dependencies returns the assets upon which this asset directly depends.
Dependencies() []Asset

// Generate generates this asset given the states of its dependent assets.
Generate(map[Asset]*State) (*State, error)
// Generate generates this asset given the states of its parent assets.
Generate(Parents) error

// Name returns the human-friendly name of the asset.
Name() string
}

// WritableAsset is an Asset that has files that can be written to disk.
type WritableAsset interface {
Asset

// Files returns the files to write.
Files() []*File
}

// File is a file for an Asset.
type File struct {
// Filename is the name of the file.
Filename string
// Data is the contents of the file.
Data []byte
}

// PersistToFile writes all of the files of the specified asset into the specified
// directory.
func PersistToFile(asset WritableAsset, directory string) error {
for _, f := range asset.Files() {
path := filepath.Join(directory, f.Filename)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return errors.Wrap(err, "failed to create dir")
}
if err := ioutil.WriteFile(path, f.Data, 0644); err != nil {
return errors.Wrap(err, "failed to write file")
}
}
return nil
}
41 changes: 32 additions & 9 deletions pkg/asset/state_test.go → pkg/asset/asset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,37 @@ import (
"github.com/stretchr/testify/assert"
)

func TestStatePersistToFile(t *testing.T) {
type persistAsset struct{}

func (a *persistAsset) Name() string {
return "persist-asset"
}

func (a *persistAsset) Dependencies() []Asset {
return []Asset{}
}

func (a *persistAsset) Generate(Parents) error {
return nil
}

type writablePersistAsset struct {
persistAsset
files []*File
}

func (a *writablePersistAsset) Files() []*File {
return a.files
}

func TestPersistToFile(t *testing.T) {
cases := []struct {
name string
filenames []string
}{
{
name: "no files",
filenames: []string{""},
filenames: []string{},
},
{
name: "single file",
Expand All @@ -40,19 +63,19 @@ func TestStatePersistToFile(t *testing.T) {
}
defer os.RemoveAll(dir)

state := &State{
Contents: make([]Content, len(tc.filenames)),
asset := &writablePersistAsset{
files: make([]*File, len(tc.filenames)),
}
expectedFiles := map[string][]byte{}
for i, filename := range tc.filenames {
data := []byte(fmt.Sprintf("data%d", i))
state.Contents[i].Name = filename
state.Contents[i].Data = data
if filename != "" {
expectedFiles[filepath.Join(dir, filename)] = data
asset.files[i] = &File{
Filename: filename,
Data: data,
}
expectedFiles[filepath.Join(dir, filename)] = data
}
err = state.PersistToFile(dir)
err = PersistToFile(asset, dir)
assert.NoError(t, err, "unexpected error persisting state to file")
verifyFilesCreated(t, dir, expectedFiles)
})
Expand Down
57 changes: 31 additions & 26 deletions pkg/asset/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/openshift/installer/data"
"github.com/openshift/installer/pkg/asset"
"github.com/openshift/installer/pkg/asset/kubeconfig"
"github.com/openshift/installer/pkg/terraform"
)

Expand All @@ -20,13 +21,10 @@ const (
// Cluster uses the terraform executable to launch a cluster
// with the given terraform tfvar and generated templates.
type Cluster struct {
// The root directory of the generated assets.
rootDir string
tfvars asset.Asset
kubeconfig asset.Asset
file *asset.File
}

var _ asset.Asset = (*Cluster)(nil)
var _ asset.WritableAsset = (*Cluster)(nil)

// Name returns the human-friendly name of the asset.
func (c *Cluster) Name() string {
Expand All @@ -36,42 +34,45 @@ func (c *Cluster) Name() string {
// Dependencies returns the direct dependency for launching
// the cluster.
func (c *Cluster) Dependencies() []asset.Asset {
return []asset.Asset{c.tfvars, c.kubeconfig}
return []asset.Asset{
&TerraformVariables{},
&kubeconfig.Admin{},
}
}

// Generate launches the cluster and generates the terraform state file on disk.
func (c *Cluster) Generate(parents map[asset.Asset]*asset.State) (*asset.State, error) {
state, ok := parents[c.tfvars]
if !ok {
return nil, errors.Errorf("failed to get terraform.tfvars from parent")
}
func (c *Cluster) Generate(parents asset.Parents) error {
terraformVariables := &TerraformVariables{}
adminKubeconfig := &kubeconfig.Admin{}
parents.Get(terraformVariables, adminKubeconfig)

// Copy the terraform.tfvars to a temp directory where the terraform will be invoked within.
tmpDir, err := ioutil.TempDir(os.TempDir(), "openshift-install-")
if err != nil {
return nil, errors.Wrap(err, "failed to create temp dir for terraform execution")
return errors.Wrap(err, "failed to create temp dir for terraform execution")
}
defer os.RemoveAll(tmpDir)

if err := ioutil.WriteFile(filepath.Join(tmpDir, state.Contents[0].Name), state.Contents[0].Data, 0600); err != nil {
return nil, errors.Wrap(err, "failed to write terraform.tfvars file")
terraformVariablesFile := terraformVariables.Files()[0]
if err := ioutil.WriteFile(filepath.Join(tmpDir, terraformVariablesFile.Filename), terraformVariablesFile.Data, 0600); err != nil {
return errors.Wrap(err, "failed to write terraform.tfvars file")
}

platform := string(state.Contents[1].Data)
platform := terraformVariables.platform
if err := data.Unpack(tmpDir, platform); err != nil {
return nil, err
return err
}

if err := data.Unpack(filepath.Join(tmpDir, "config.tf"), "config.tf"); err != nil {
return nil, err
return err
}

logrus.Infof("Using Terraform to create cluster...")

// This runs the terraform in a temp directory, the tfstate file will be returned
// to the asset store to persist it on the disk.
if err := terraform.Init(tmpDir); err != nil {
return nil, errors.Wrap(err, "failed to initialize terraform")
return errors.Wrap(err, "failed to initialize terraform")
}

stateFile, err := terraform.Apply(tmpDir)
Expand All @@ -88,12 +89,16 @@ func (c *Cluster) Generate(parents map[asset.Asset]*asset.State) (*asset.State,
}
}

return &asset.State{
Contents: []asset.Content{
{
Name: stateFileName,
Data: data,
},
},
}, err
c.file = &asset.File{
Filename: stateFileName,
Data: data,
}

// TODO(yifan): Use the kubeconfig to verify the cluster is up.
return nil
}

// Files returns the files generated by the asset.
func (c *Cluster) Files() []*asset.File {
return []*asset.File{c.file}
}
Loading