airshipui/pkg/ctl/document.go
Schiefelbein, Andrew 85e8583043 Visualization of baremetal CTL component
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
2020-10-13 10:57:32 -05:00

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), &params)
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
}