diff --git a/cmd/node-joiner/main.go b/cmd/node-joiner/main.go index 38505690206..dc80da340dc 100644 --- a/cmd/node-joiner/main.go +++ b/cmd/node-joiner/main.go @@ -19,7 +19,11 @@ func main() { Use: "add-nodes", Short: "Generates an ISO that could be used to boot the configured nodes to let them join an existing cluster", RunE: func(cmd *cobra.Command, args []string) error { - return nodejoiner.NewAddNodesCommand(wd) + kubeConfig, err := cmd.Flags().GetString("kubeconfig") + if err != nil { + return err + } + return nodejoiner.NewAddNodesCommand(wd, kubeConfig) }, } @@ -34,6 +38,7 @@ func main() { rootCmd := &cobra.Command{ Use: "node-joiner", } + rootCmd.PersistentFlags().String("kubeconfig", "", "Path to the kubeconfig file.") rootCmd.AddCommand(nodesAddCmd) rootCmd.AddCommand(nodesMonitorCmd) diff --git a/pkg/asset/agent/joiner/addnodesconfig.go b/pkg/asset/agent/joiner/addnodesconfig.go new file mode 100644 index 00000000000..35a67efdb09 --- /dev/null +++ b/pkg/asset/agent/joiner/addnodesconfig.go @@ -0,0 +1,80 @@ +package joiner + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/openshift/installer/pkg/asset" +) + +const ( + addNodesParamsFile = ".addnodesparams" +) + +// AddNodesConfig is used to store the current configuration +// for the command. +type AddNodesConfig struct { + File *asset.File + Params Params +} + +// Params is used to store the command line parameters. +type Params struct { + Kubeconfig string `json:"kubeconfig,omitempty"` +} + +// Save stores the current parameters on disk. +func (p *Params) Save(assetsDir string) error { + data, err := json.Marshal(p) + if err != nil { + return err + } + + fileName := filepath.Join(assetsDir, addNodesParamsFile) + return os.WriteFile(fileName, data, 0o600) +} + +// Name returns the human-friendly name of the asset. +func (*AddNodesConfig) Name() string { + return "AddNodes Config" +} + +// Dependencies returns all of the dependencies directly needed to generate +// the asset. +func (*AddNodesConfig) Dependencies() []asset.Asset { + return []asset.Asset{} +} + +// Generate it's empty for this asset, always loaded from disk. +func (*AddNodesConfig) Generate(dependencies asset.Parents) error { + return nil +} + +// Files returns the files generated by the asset. +func (a *AddNodesConfig) Files() []*asset.File { + if a.File != nil { + return []*asset.File{a.File} + } + return []*asset.File{} +} + +// Load returns agent config asset from the disk. +func (a *AddNodesConfig) Load(f asset.FileFetcher) (bool, error) { + file, err := f.FetchByName(addNodesParamsFile) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, fmt.Errorf("failed to load %s file: %w", addNodesParamsFile, err) + } + + params := &Params{} + if err := json.Unmarshal(file.Data, params); err != nil { + return false, fmt.Errorf("failed to unmarshal %s: %w", addNodesParamsFile, err) + } + + a.Params = *params + return true, nil +} diff --git a/pkg/asset/agent/joiner/clusterinfo.go b/pkg/asset/agent/joiner/clusterinfo.go new file mode 100644 index 00000000000..877dce2c713 --- /dev/null +++ b/pkg/asset/agent/joiner/clusterinfo.go @@ -0,0 +1,135 @@ +package joiner + +import ( + "context" + "net/url" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + configclient "github.com/openshift/client-go/config/clientset/versioned" + "github.com/openshift/installer/pkg/asset" + "github.com/openshift/installer/pkg/asset/agent/workflow" +) + +// ClusterInfo it's an asset used to retrieve config info +// from an already existing cluster. +type ClusterInfo struct { + ClusterID string + APIDNSName string + PullSecret string +} + +var _ asset.WritableAsset = (*ClusterInfo)(nil) + +// Name returns the human-friendly name of the asset. +func (ci *ClusterInfo) Name() string { + return "Agent Installer ClusterInfo" +} + +// Dependencies returns all of the dependencies directly needed to generate +// the asset. +func (*ClusterInfo) Dependencies() []asset.Asset { + return []asset.Asset{ + &workflow.AgentWorkflow{}, + &AddNodesConfig{}, + } +} + +// Generate generates the ClusterInfo. +func (ci *ClusterInfo) Generate(dependencies asset.Parents) error { + agentWorkflow := &workflow.AgentWorkflow{} + addNodesConfig := &AddNodesConfig{} + dependencies.Get(agentWorkflow, addNodesConfig) + + if agentWorkflow.Workflow != workflow.AgentWorkflowTypeAddNodes { + return nil + } + + config, err := ci.getRestConfig(addNodesConfig.Params.Kubeconfig) + if err != nil { + return err + } + + err = ci.retrieveClusterID(config) + if err != nil { + return err + } + + err = ci.retrieveAPIDNSName(config) + if err != nil { + return err + } + + err = ci.retrievePullSecret(config) + if err != nil { + return err + } + + return nil +} + +func (ci *ClusterInfo) getRestConfig(kubeconfig string) (*rest.Config, error) { + var err error + var config *rest.Config + + if kubeconfig != "" { + config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) + } else { + config, err = rest.InClusterConfig() + } + + return config, err +} + +func (ci *ClusterInfo) retrieveClusterID(config *rest.Config) error { + clientset, err := configclient.NewForConfig(config) + if err != nil { + return err + } + + cv, err := clientset.ConfigV1().ClusterVersions().Get(context.Background(), "version", metav1.GetOptions{}) + if err != nil { + return err + } + ci.ClusterID = string(cv.Spec.ClusterID) + + return nil +} + +func (ci *ClusterInfo) retrieveAPIDNSName(config *rest.Config) error { + parsedURL, err := url.Parse(config.Host) + if err != nil { + return err + } + + ci.APIDNSName = parsedURL.Hostname() + return nil +} + +func (ci *ClusterInfo) retrievePullSecret(config *rest.Config) error { + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return err + } + + pullSecret, err := clientset.CoreV1().Secrets("openshift-config").Get(context.Background(), "pull-secret", metav1.GetOptions{}) + if err != nil { + return err + } + ci.PullSecret = string(pullSecret.Data[".dockerconfigjson"]) + + return nil +} + +// Files returns the files generated by the asset. +func (*ClusterInfo) Files() []*asset.File { + return []*asset.File{} +} + +// Load returns agent config asset from the disk. +func (*ClusterInfo) Load(f asset.FileFetcher) (bool, error) { + return false, nil +} diff --git a/pkg/asset/agent/workflow/agentworkflow.go b/pkg/asset/agent/workflow/agentworkflow.go new file mode 100644 index 00000000000..df8f82854b8 --- /dev/null +++ b/pkg/asset/agent/workflow/agentworkflow.go @@ -0,0 +1,65 @@ +package workflow + +import ( + "fmt" + "os" + + "github.com/openshift/installer/pkg/asset" +) + +// AgentWorkflow allows other assets to check +// which is the workflow currently active. +type AgentWorkflow struct { + File *asset.File + Workflow AgentWorkflowType +} + +var _ asset.WritableAsset = (*AgentWorkflow)(nil) + +// Name returns a human friendly name for the asset. +func (*AgentWorkflow) Name() string { + return "Agent Workflow" +} + +// Dependencies returns all of the dependencies directly needed to generate +// the asset. +func (*AgentWorkflow) Dependencies() []asset.Asset { + return []asset.Asset{} +} + +// Generate generates the AgentWorkflow asset. +func (a *AgentWorkflow) Generate(dependencies asset.Parents) error { + // Set install workflow as a default + a.Workflow = AgentWorkflowTypeInstall + a.File = &asset.File{ + Filename: agentWorkflowFilename, + Data: []byte(a.Workflow), + } + + return nil +} + +// Files returns the files generated by the asset. +func (a *AgentWorkflow) Files() []*asset.File { + if a.File != nil { + return []*asset.File{a.File} + } + return []*asset.File{} +} + +// Load returns the asset from disk. +func (a *AgentWorkflow) Load(f asset.FileFetcher) (bool, error) { + file, err := f.FetchByName(agentWorkflowFilename) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, fmt.Errorf("failed to load %s file: %w", agentWorkflowFilename, err) + } + + // Get the current workflow + a.Workflow = AgentWorkflowType(file.Data) + a.File = file + + return true, nil +} diff --git a/pkg/asset/agent/workflow/agentworkflowaddnodes.go b/pkg/asset/agent/workflow/agentworkflowaddnodes.go new file mode 100644 index 00000000000..347b4fd8d41 --- /dev/null +++ b/pkg/asset/agent/workflow/agentworkflowaddnodes.go @@ -0,0 +1,27 @@ +package workflow + +import "github.com/openshift/installer/pkg/asset" + +// AgentWorkflowAddNodes is meant just to define +// the add nodes workflow. +type AgentWorkflowAddNodes struct { + AgentWorkflow +} + +var _ asset.WritableAsset = (*AgentWorkflowAddNodes)(nil) + +// Name returns a human friendly name for the asset. +func (*AgentWorkflowAddNodes) Name() string { + return "Agent Workflow Add Nodes" +} + +// Generate generates the AgentWorkflow asset. +func (a *AgentWorkflowAddNodes) Generate(dependencies asset.Parents) error { + a.Workflow = AgentWorkflowTypeAddNodes + a.File = &asset.File{ + Filename: agentWorkflowFilename, + Data: []byte(a.Workflow), + } + + return nil +} diff --git a/pkg/asset/agent/workflow/commons.go b/pkg/asset/agent/workflow/commons.go new file mode 100644 index 00000000000..2bfed30d7b3 --- /dev/null +++ b/pkg/asset/agent/workflow/commons.go @@ -0,0 +1,14 @@ +package workflow + +// AgentWorkflowType defines the supported +// agent workflows. +type AgentWorkflowType string + +const ( + // AgentWorkflowTypeInstall identifies the install workflow. + AgentWorkflowTypeInstall AgentWorkflowType = "install" + // AgentWorkflowTypeAddNodes identifies the add nodes workflow. + AgentWorkflowTypeAddNodes AgentWorkflowType = "addnodes" + + agentWorkflowFilename = ".agentworkflow" +) diff --git a/pkg/nodejoiner/addnodes.go b/pkg/nodejoiner/addnodes.go index e35f7dd6d1d..94a50200feb 100644 --- a/pkg/nodejoiner/addnodes.go +++ b/pkg/nodejoiner/addnodes.go @@ -2,11 +2,26 @@ package nodejoiner import ( "github.com/openshift/installer/pkg/asset" + "github.com/openshift/installer/pkg/asset/agent/joiner" + "github.com/openshift/installer/pkg/asset/agent/workflow" "github.com/openshift/installer/pkg/asset/store" ) // NewAddNodesCommand creates a new command for add nodes. -func NewAddNodesCommand(directory string) error { +func NewAddNodesCommand(directory string, kubeConfig string) error { + // Store the current parameters into the assets folder, so + // that they could be retrieved later by the assets + params := joiner.Params{ + Kubeconfig: kubeConfig, + } + err := params.Save(directory) + if err != nil { + return err + } + fetcher := store.NewAssetsFetcher(directory) - return fetcher.FetchAndPersist([]asset.WritableAsset{}) + return fetcher.FetchAndPersist([]asset.WritableAsset{ + &workflow.AgentWorkflowAddNodes{}, + // To be completed + }) }