Commit e819ed91 authored by lukas.burgey's avatar lukas.burgey

Merge branch 'v3'

parents 3da1bbd5 b08fe52e
FEUDAL Scripts v2
FEUDAL Scripts Version 3
=
FEUDAL scripts (sometimes also called adapters) are executed by a [feudalClient](https://git.scc.kit.edu/feudal/feudalClient) to facilitate the customized deployment process
of a service.
FEUDAL scripts (also called adapters) are used to deploy feudal users in a customizable fashion. They are executed by a [feudalClient](https://git.scc.kit.edu/feudal/feudalClient) and have a specific input and output.
The scripts use the JSON encoding for input and output. The specific formats are loosely outlined below.
The input is passed to the scripts via stdin.
This go library can be used as a basis to implement such a script.
Examples can be found here: [a simple stub](stubScript/stub-script.go), [creating SSH access](sshScript/ssh-script.go), and [handling questionnaires](questionnaireScript/questionnaire-script.go).
The [feudalClient](https://git.scc.kit.edu/feudal/feudalClient) can be used to generate and validate JSON schema for input
The scripts use json for input and output. The specific formats are outlined
below. The input is passed to the scripts via stdin.
The [feudalClient](https://git.scc.kit.edu/feudal/feudalClient) can be used to generate and validate json schema for input
and output, see:
```
feudalClient schema --help
feudalClient validate --help
```
Input Format of v2
Input Format
-
```
{
......@@ -44,13 +46,17 @@ Input Format of v2
// Answers to a previously requested questionnaire, may not be present
"answers": {
"question_name": "user answer to this question"
"question_name": "user answer to this question",
"age_question": 18,
"list_question": "person_a",
"list_question_2": 2,
"are_you_sure": true
}
}
```
Output Format of v2
Output Format
-
```
{
......@@ -70,7 +76,21 @@ Output Format of v2
// questions in this dictionary.
// The user can answer these questions (and we will receive the answers some input in the future)
"questionnaire": {
"question_name": "question"
"question_name": "question",
"age_question": "How old are you?",
"list_question": "Who are you?",
"list_question_2": "How many do you want?",
"are_you_sure": "What you are trying is wrong. Are you sure?"
},
// By default questions in questionnaire expect answers are strings. You can change this here:
// Add a mapping with the same key here
"questionnaire_answers": {
"question_name": "question", // string default value
"age_question": 18, // age_question must be an integer, defaulting to 18
"list_question": ["person_a", "person_b"], // list_question must be one of the listed options
"list_question_2": [1, 2], // list_question_2 must be one of the listed options
"are_you_sure": false // are_you_sure must be a boolean, with false being the default value
},
// additional credentials and instructions, the user needs to access the service (in addition to her credentials from the Input)
......
module git.scc.kit.edu/feudal/feudalScripts/v3
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