
This allows for both the nodes and phases to have baremetal actions taken against them. It is using angular material tables to render the data on the screen and hides / shows the nodes or phases based on the user interaction. This may need some improvements but it is working for all the testable functions. Change-Id: Icb9e1f14735d96b37758e90c2ec9973279022b9e
451 lines
10 KiB
Go
451 lines
10 KiB
Go
/*
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package ctl
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/google/uuid"
|
|
"opendev.org/airship/airshipctl/pkg/config"
|
|
"opendev.org/airship/airshipctl/pkg/document"
|
|
"opendev.org/airship/airshipctl/pkg/document/pull"
|
|
"opendev.org/airship/airshipctl/pkg/phase"
|
|
"opendev.org/airship/airshipctl/pkg/phase/ifc"
|
|
"opendev.org/airship/airshipui/pkg/configs"
|
|
)
|
|
|
|
var (
|
|
fileIndex map[string]string
|
|
docIndex map[string]document.Document
|
|
)
|
|
|
|
// HandleDocumentRequest will flop between requests so we don't have to have them all mapped as function calls
|
|
func HandleDocumentRequest(user *string, request configs.WsMessage) configs.WsMessage {
|
|
response := configs.WsMessage{
|
|
Type: configs.CTL,
|
|
Component: configs.Document,
|
|
SubComponent: request.SubComponent,
|
|
}
|
|
|
|
var err error
|
|
var message *string
|
|
var id string
|
|
|
|
client, err := NewClient(AirshipConfigPath, KubeConfigPath, request)
|
|
if err != nil {
|
|
e := err.Error()
|
|
response.Error = &e
|
|
return response
|
|
}
|
|
|
|
switch request.SubComponent {
|
|
case configs.Pull:
|
|
message, err = client.docPull()
|
|
case configs.Plugin:
|
|
err = fmt.Errorf("Subcomponent %s not implemented", request.SubComponent)
|
|
case configs.YamlWrite:
|
|
id = request.ID
|
|
response.Name, response.YAML, err = client.writeYamlFile(id, request.YAML)
|
|
s := fmt.Sprintf("File '%s' saved successfully", response.Name)
|
|
message = &s
|
|
case configs.GetYaml:
|
|
id = request.ID
|
|
message = request.Message
|
|
response.Name, response.YAML, err = client.getYaml(id, *message)
|
|
case configs.GetPhaseTree:
|
|
response.Data, err = client.GetPhaseTree()
|
|
case configs.GetPhase:
|
|
id = request.ID
|
|
s := "rendered"
|
|
message = &s
|
|
response.Name, response.Details, response.YAML, err = client.GetPhase(id)
|
|
case configs.GetDocumentsBySelector:
|
|
id = request.ID
|
|
response.Data, err = GetDocumentsBySelector(request.ID, *request.Message)
|
|
case configs.GetTarget:
|
|
message = client.getTarget()
|
|
case configs.GetExecutorDoc:
|
|
id = request.ID
|
|
s := "rendered"
|
|
message = &s
|
|
response.Name, response.YAML, err = client.GetExecutorDoc(id)
|
|
default:
|
|
err = fmt.Errorf("Subcomponent %s not found", request.SubComponent)
|
|
}
|
|
|
|
if err != nil {
|
|
e := err.Error()
|
|
response.Error = &e
|
|
} else {
|
|
response.Message = message
|
|
response.ID = id
|
|
}
|
|
|
|
return response
|
|
}
|
|
|
|
// GetExecutorDoc returns the title and YAML of the executor document for the specified phase
|
|
func (c *Client) GetExecutorDoc(id string) (string, string, error) {
|
|
helper, err := getHelper()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
phaseID := ifc.ID{}
|
|
|
|
err = json.Unmarshal([]byte(id), &phaseID)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
ed, err := helper.ExecutorDoc(phaseID)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
title := ed.GetName()
|
|
bytes, err := ed.AsYAML()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
return title, base64.StdEncoding.EncodeToString(bytes), nil
|
|
}
|
|
|
|
func (c *Client) getTarget() *string {
|
|
var s string
|
|
m, err := c.Config.CurrentContextManifest()
|
|
if err != nil {
|
|
s = "unknown"
|
|
return &s
|
|
}
|
|
|
|
s = filepath.Join(m.TargetPath, m.SubPath)
|
|
return &s
|
|
}
|
|
|
|
func (c *Client) getPhaseDetails(id ifc.ID) (string, error) {
|
|
helper, err := getHelper()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
pClient := phase.NewClient(helper)
|
|
|
|
phaseIfc, err := pClient.PhaseByID(id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return phaseIfc.Details()
|
|
}
|
|
|
|
func (c *Client) getYaml(id, message string) (string, string, error) {
|
|
switch message {
|
|
case "source":
|
|
name, yaml, err := c.getFileYaml(id)
|
|
return name, yaml, err
|
|
case "rendered":
|
|
name, yaml, err := c.getDocumentYaml(id)
|
|
return name, yaml, err
|
|
default:
|
|
return "", "", fmt.Errorf("'%s' unrecognized document type", message)
|
|
}
|
|
}
|
|
|
|
func (c *Client) getDocumentYaml(id string) (string, string, error) {
|
|
doc, ok := docIndex[id]
|
|
if !ok {
|
|
return "", "", fmt.Errorf("document with ID '%s' not found", id)
|
|
}
|
|
title := doc.GetName()
|
|
bytes, err := doc.AsYAML()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
return title, base64.StdEncoding.EncodeToString(bytes), nil
|
|
}
|
|
|
|
func (c *Client) getFileYaml(id string) (string, string, error) {
|
|
path, ok := fileIndex[id]
|
|
if !ok {
|
|
return "", "", fmt.Errorf("file with ID '%s' not found", id)
|
|
}
|
|
|
|
ccm, err := c.Config.CurrentContextManifest()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
// this is making the assumption that the site definition
|
|
// will always found at: targetPath/subPath
|
|
sitePath := filepath.Join(ccm.TargetPath, ccm.SubPath)
|
|
|
|
// TODO(mfuller): will this be true in treasuremap or
|
|
// other external repos?
|
|
manifestsDir := filepath.Join(sitePath, "..", "..")
|
|
|
|
title, err := filepath.Rel(manifestsDir, path)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
bytes, err := ioutil.ReadAll(file)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
return title, base64.StdEncoding.EncodeToString(bytes), nil
|
|
}
|
|
|
|
func (c *Client) writeYamlFile(id, yaml64 string) (string, string, error) {
|
|
path, ok := fileIndex[id]
|
|
if !ok {
|
|
return "", "", fmt.Errorf("ID %s not found", id)
|
|
}
|
|
|
|
yaml, err := base64.StdEncoding.DecodeString(yaml64)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
err = ioutil.WriteFile(path, yaml, 0600)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
return c.getFileYaml(id)
|
|
}
|
|
|
|
func getPhaseBundle(id ifc.ID) (document.Bundle, error) {
|
|
helper, err := getHelper()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pClient := phase.NewClient(helper)
|
|
|
|
phaseIfc, err := pClient.PhaseByID(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
docRoot, err := phaseIfc.DocumentRoot()
|
|
if err != nil {
|
|
// if phase has no doc entrypoint defined, just
|
|
// return nothing; otherwise, return the error
|
|
if errors.As(err, &phase.ErrDocumentEntrypointNotDefined{}) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
b, err := document.NewBundleByPath(docRoot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// GetPhase returns the name, description, and doc bundle for specified phase
|
|
func (c *Client) GetPhase(id string) (string, string, string, error) {
|
|
phaseID := ifc.ID{}
|
|
|
|
err := json.Unmarshal([]byte(id), &phaseID)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
|
|
title := phaseID.Name
|
|
|
|
details, err := c.getPhaseDetails(phaseID)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
|
|
bundle, err := getPhaseBundle(phaseID)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
|
|
// only return title if phase has no bundle
|
|
if bundle == nil {
|
|
return title, details, "", nil
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = bundle.Write(&buf)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
|
|
return title, details, base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
|
}
|
|
|
|
func (c *Client) docPull() (*string, error) {
|
|
var message *string
|
|
cfgFactory := config.CreateFactory(AirshipConfigPath, KubeConfigPath)
|
|
// 2nd arg is noCheckout, I assume we want to checkout the repo,
|
|
// so setting to false
|
|
err := pull.Pull(cfgFactory, false)
|
|
if err == nil {
|
|
s := fmt.Sprintf("Success")
|
|
message = &s
|
|
}
|
|
|
|
return message, err
|
|
}
|
|
|
|
// SelectorParams structure to hold data for constructing a document Selector
|
|
type SelectorParams struct {
|
|
Name string `json:"name,omitempty"`
|
|
Namespace string `json:"namespace,omitempty"`
|
|
GVK GVK `json:"gvk,omitempty"`
|
|
Kind string `json:"kind,omitempty"`
|
|
Label string `json:"label,omitempty"`
|
|
Annotation string `json:"annotation,omitempty"`
|
|
}
|
|
|
|
// GVK small structure to hold group, version, kind for building a Selector
|
|
type GVK struct {
|
|
Group string `json:"group"`
|
|
Version string `json:"version"`
|
|
Kind string `json:"kind"`
|
|
}
|
|
|
|
// GetDocumentsBySelector returns a slice of KustomNodes representing all phase
|
|
// documents returned by applying the provided Selector
|
|
func GetDocumentsBySelector(id string, data string) ([]KustomNode, error) {
|
|
docIndex = map[string]document.Document{}
|
|
|
|
selector, err := getSelector(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
phaseID := ifc.ID{}
|
|
err = json.Unmarshal([]byte(id), &phaseID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
helper, err := getHelper()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pClient := phase.NewClient(helper)
|
|
|
|
phaseIfc, err := pClient.PhaseByID(phaseID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
docRoot, err := phaseIfc.DocumentRoot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bundle, err := document.NewBundleByPath(docRoot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
docs, err := bundle.Select(selector)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
results := []KustomNode{}
|
|
|
|
for _, doc := range docs {
|
|
// this is a workaround for a kustomize issue where cluster-scoped objects
|
|
// are included in matching results when a namespace selector is specified
|
|
// (https://github.com/kubernetes-sigs/kustomize/issues/2248)
|
|
if selector.Namespace != "" && selector.Namespace != doc.GetNamespace() {
|
|
continue
|
|
}
|
|
|
|
id := uuid.New().String()
|
|
docIndex[id] = doc
|
|
|
|
name := doc.GetNamespace()
|
|
if name == "" {
|
|
name = "[none]"
|
|
}
|
|
|
|
results = append(results, KustomNode{
|
|
ID: id,
|
|
Name: fmt.Sprintf("%s/%s/%s",
|
|
name,
|
|
doc.GetKind(),
|
|
doc.GetName(),
|
|
)},
|
|
)
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func getSelector(data string) (document.Selector, error) {
|
|
params := SelectorParams{}
|
|
err := json.Unmarshal([]byte(data), ¶ms)
|
|
if err != nil {
|
|
return document.Selector{}, err
|
|
}
|
|
|
|
s := document.NewSelector()
|
|
|
|
// build selector based on what we were given
|
|
if params.Name != "" {
|
|
s = s.ByName(params.Name)
|
|
}
|
|
if params.Namespace != "" {
|
|
s = s.ByNamespace(params.Namespace)
|
|
}
|
|
if (GVK{}) != params.GVK {
|
|
s = s.ByGvk(
|
|
params.GVK.Group,
|
|
params.GVK.Version,
|
|
params.GVK.Kind,
|
|
)
|
|
}
|
|
if params.Kind != "" {
|
|
s = s.ByKind(params.Kind)
|
|
}
|
|
if params.Label != "" {
|
|
s = s.ByLabel(params.Label)
|
|
}
|
|
if params.Annotation != "" {
|
|
s = s.ByAnnotation(params.Annotation)
|
|
}
|
|
return s, nil
|
|
}
|