Commit 020102d8 authored by lukas.burgey's avatar lukas.burgey

Add v3

parent 3da1bbd5
module git.scc.kit.edu/feudal/feudalScripts/v2
go 1.12
package scripts
import (
"bytes"
"encoding/json"
"fmt"
"log"
)
type (
// Credential is currently a ssh key, but may be a password hash in the future
Credential struct {
ID int `json:"id,omitempty"`
Type string `json:"type,omitempty"`
Name string `json:"name"`
Value string `json:"value"`
}
// UserCredentialStates serves to inform the backend about the per credential states
// after the script run
// This maps a credential type like "ssh_key" to a map of the states of credentials of this
// type.
UserCredentialStates map[string]map[string]State
// UserCredentials maps a credential type to the credentials of this type
UserCredentials map[string][]Credential
// UserInfo info about the user
UserInfo map[string]interface{}
// User contains information concerning the user of a deployment
User struct {
UserInfo UserInfo `json:"userinfo"`
Credentials UserCredentials `json:"credentials"`
}
// Input of the deployment script
Input struct {
// StateTarget is the state which is to be reached by this deployment task
// StateTarget is either Deployed or NotDeployed
StateTarget State `json:"state_target"`
// User describes the user of this deployment task
User User `json:"user"`
// Answers is an answered questionnaire relating to an Output.Questionnaire
//
// Maps question names (corresponding to the Output.Questionnaire) to the answers of the user.
// The type of an answer must be string, if there is no mapping for the key in the corresponding Output.QuestionnaireAnswers.
// If there is such a mapping the answer type must be of the type indicated by the mapping. See Output.QuestionnaireAnswers for details.
Answers map[string]interface{} `json:"answers,omitempty"`
}
// Output of the deployment script
Output struct {
// State describes the state of the deployment, after the script execution.
// When State == Questionnaire then Output.Questionnaire *must* be set.
// When State == Questionnaire then Output.QuestionnaireAnswers *can* be set.
// When State == Deployed then Output.Credentials *can* be set.
State State `json:"state"`
// Msg is a message for the user.
Msg string `json:"message"`
// Questionnaire allows the script to requets further information from the user.
//
// This field is Ignored when Output.State is not Questionnaire.
// Questionnaire maps a question name to a question text.
Questionnaire map[string]string `json:"questionnaire,omitempty"`
// QuestionnaireAnswers allows the script to control which answers the user can give.
//
// This field is Ignored when Output.State is not Questionnaire.
// QuestionnaireAnswers maps a question name (corresponding to the question names in Output.Questionnaire) to one of the following:
// - a boolean value indicating a yes/no question with the default selection being the value itself
// - a list of strings or integers indicating that the user must select one of the options of the list
// - a string value indicating that the answer must be of type string. The value itself will be set as default answer for the user.
//
// Providing a mapping in QuestionnaireAnswers for a question is optional. If no mapping is provided the default answer must be a string.
QuestionnaireAnswers map[string]interface{} `json:"questionnaire_answers,omitempty"`
// Credentials are additionnal credentials for the user.
// Examples are additional passwords.
// This field is ignored by the client when Output.State is not Deployed.
// Credentials maps a credential name to a credential value.
Credentials map[string]string `json:"credentials,omitempty"`
// UserCredentialStates are the State s of the credentials found in Input.User.Credentials.
// This field is not mandatory. The client will assume that all credentials have the State
// Output.State if this field is not given.
UserCredentialStates UserCredentialStates `json:"user_credential_states,omitempty"`
}
// State is a string enum
// The enum values for State are listed below
State string
)
const (
// Deployed value for State
Deployed State = "deployed"
// NotDeployed value for State
NotDeployed State = "not_deployed"
// Rejected value for State
// the user can never be deployed
Rejected State = "rejected"
// Failed value for State
// the deployment can be retried
Failed State = "failed"
// Questionnaire value for State
// the user has to fill the questionnaire
// with the questionnaire data the deployment can be retried
Questionnaire State = "questionnaire"
)
func (u User) String() string {
if email, ok := u.UserInfo["email"]; ok {
return email.(string)
}
if name, ok := u.UserInfo["name"]; ok {
return name.(string)
}
return ""
}
func (i Input) String() string {
iBytes, err := i.Marshal()
if err != nil {
log.Fatal(err)
}
return string(iBytes)
}
func (o Output) String() string {
oBytes, err := o.Marshal()
if err != nil {
log.Fatal(err)
}
return string(oBytes)
}
// Marshal encodes an Input as json
func (i Input) Marshal() (iBytes []byte, err error) {
iBytes, err = json.MarshalIndent(i, "", " ")
return
}
// Marshal encodes an Output as json
func (o Output) Marshal() (oBytes []byte, err error) {
oBytes, err = json.MarshalIndent(o, "", " ")
return
}
// UnmarshalInput decodes a json encoded input and does some minor sanity checking
func UnmarshalInput(inputBytes []byte, i *Input) (err error) {
// check if state_target exists
if bytes.Index(inputBytes, []byte(`"state_target"`)) < 0 {
err = fmt.Errorf("Output does not contain the field 'state'")
return
}
err = json.Unmarshal(inputBytes, i)
return
}
// UnmarshalOutput decodes a json encoded output and does some minor sanity checking
func UnmarshalOutput(inputBytes []byte, o *Output) (err error) {
if o == nil {
err = fmt.Errorf("Output pointer is nil")
return
}
// check if state exists
if bytes.Index(inputBytes, []byte(`"state"`)) < 0 {
err = fmt.Errorf("Output does not contain the field 'state'")
return
}
// unmarshal json
err = json.Unmarshal(inputBytes, o)
if err != nil {
return
}
// check questionnaire sanity
for qName, answer := range o.QuestionnaireAnswers {
if _, ok := o.Questionnaire[qName]; !ok {
err = fmt.Errorf("QuestionnaireAnswers contains key '%s' which is missing in Questionnaire", qName)
return
}
// check answer type
switch t := answer.(type) {
case string:
break
case int:
break
case bool:
break
case []string:
break
case []int:
break
default:
err = fmt.Errorf("QuestionnaireAnswers contains invalid answer type: %v (%v)", t, answer)
}
}
return
}
// SanityCheck checks if o is a sane Output of a script when i s the corresponding input
func SanityCheck(i *Input, o *Output) error {
// check o.UserCredentialStates against i.User.UserCredentials
if len(o.UserCredentialStates) > 0 {
// check each credential type
for oCredentialType, oCredentialMap := range o.UserCredentialStates {
iCredentials, ok := i.User.Credentials[oCredentialType]
if !ok {
return fmt.Errorf("Credential type %s did not exist in the Input", oCredentialType)
}
// in Input credentials are stored in a list, but in the Output as a dictionary :/
// check if the credentials exist in the input
for oCredentialName := range oCredentialMap {
found := false
for _, iCredential := range iCredentials {
if iCredential.Name == oCredentialName {
found = true
break
}
}
if !found {
return fmt.Errorf("Credential %s of type %s did not exist in the Input", oCredentialName, oCredentialType)
}
}
}
}
// TODO expand this check
return nil
}
package scripts
import (
"encoding/json"
"fmt"
"testing"
)
func checkOutput(o *Output) (err error) {
if o.Msg == "" {
return fmt.Errorf("Output.Msg is empty")
}
return
}
func testOutput(rawOutput []byte, t *testing.T) {
var err error
output := new(Output)
err = json.Unmarshal(rawOutput, output)
if err != nil {
t.Errorf("Unmarshal failed: %s", err)
}
err = checkOutput(output)
if err != nil {
t.Errorf("checkOutput failed: %s", err)
}
}
func TestArbitraryQuestionnaire(t *testing.T) {
rawOutputs := [][]byte{
[]byte(`{
"message": "foo",
"questionnaire": {
"A string question": "answer"
}
}`),
[]byte(`{
"message": "foo",
"questionnaire": {
"An integer question": 0
}
}`),
[]byte(`{
"message": "foo",
"questionnaire": {
"A boolean question": false
}
}`),
[]byte(`{
"message": "foo",
"questionnaire": {
"A selector question": ["option a", "option b"]
}
}`),
}
for _, rawOutput := range rawOutputs {
testOutput(rawOutput, t)
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment