├── scripts ├── header.go.txt ├── logs ├── all ├── push ├── test ├── refresh-mod ├── generate-profiles ├── format ├── nuke-namespace ├── install-bash-completion ├── pull ├── _env ├── install-tools ├── delete ├── publish-container-image ├── build ├── _trap ├── generate-apis ├── deploy ├── build-container-image ├── release └── save-portable-container-image ├── CNAME ├── _config.yml ├── lab ├── linphone │ ├── .gitignore │ ├── _env │ ├── linphone-central │ ├── linphonec-edge │ ├── linphonerc-central.template │ └── linphonerc-edge.template ├── README.md ├── watch ├── openshift │ ├── shell │ ├── prepare │ ├── README.md │ └── test ├── minikube │ ├── prepare │ ├── prepare-multi │ ├── test │ └── test-multi ├── configure └── connect-kube-dns ├── examples ├── helm │ ├── artifacts │ │ └── .gitignore │ ├── hello-world │ │ ├── templates │ │ │ ├── serviceaccount.yaml │ │ │ ├── service.yaml │ │ │ ├── tests │ │ │ │ └── test-connection.yaml │ │ │ ├── hpa.yaml │ │ │ ├── ingress.yaml │ │ │ ├── NOTES.txt │ │ │ ├── _helpers.tpl │ │ │ └── deployment.yaml │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ └── values.yaml │ ├── scripts │ │ ├── build-chart │ │ └── build-csar │ ├── helm.yaml │ └── README.md ├── self-contained │ ├── artifacts │ │ ├── .gitignore │ │ ├── scripts │ │ │ └── configure.sh │ │ └── scriptlets │ │ │ └── set-output.js │ ├── scripts │ │ ├── save-container-image │ │ └── build-csar │ └── README.md ├── telephony-network-service │ ├── artifacts │ │ ├── .gitignore │ │ ├── asterisk │ │ │ ├── rtp.conf │ │ │ ├── extensions-cnf.conf.template │ │ │ ├── extensions-vnf.conf.template │ │ │ ├── pjsip-cnf.conf.template │ │ │ └── pjsip-vnf.conf.template │ │ ├── keypairs │ │ │ ├── admin@asterisk-vnf.pub │ │ │ └── admin@asterisk-vnf │ │ ├── cloud-config │ │ │ └── asterisk-vnf.yaml │ │ └── scripts │ │ │ └── asterisk │ │ │ ├── configure-cnf.sh │ │ │ └── configure-vnf.sh │ ├── scripts │ │ ├── ssh-vnf │ │ ├── register-csars │ │ ├── build-asterisk-cnf-container-image │ │ ├── build-asterisk-vnf-container-image │ │ └── build-csars │ ├── profiles │ │ ├── telephony │ │ │ └── profile.yaml │ │ └── network-service │ │ │ └── profile.yaml │ └── simple-data-plane.yaml ├── README.md ├── build-csars └── hello-world │ ├── README.md │ ├── artifacts │ ├── scripts │ │ └── configure.sh │ └── scriptlets │ │ └── set-output.js │ └── scripts │ └── build-csar ├── turandot ├── README.md ├── commands │ ├── version.go │ ├── service.go │ ├── delegate.go │ ├── operator.go │ ├── template.go │ ├── operator-status.go │ ├── operator-shell.go │ ├── operator-uninstall.go │ ├── operator-logs.go │ ├── common.go │ ├── service-clout.go │ ├── shell.go │ ├── operator-install.go │ ├── template-pull.go │ ├── service-output.go │ ├── service-mode.go │ ├── delegate-delete.go │ ├── delegate-list.go │ ├── logs.go │ ├── service-delete.go │ ├── delegate-set.go │ ├── template-delist.go │ ├── client.go │ ├── template-register.go │ ├── template-list.go │ ├── service-list.go │ └── root.go └── main.go ├── .gitignore ├── resources ├── README.md └── turandot.puccini.cloud │ ├── common.go │ └── v1alpha1 │ ├── doc.go │ └── scheme.go ├── assets ├── media │ ├── download.png │ └── telephony-network-service.png ├── tosca │ └── profiles │ │ ├── kubernetes │ │ ├── 1.0 │ │ │ ├── js │ │ │ │ ├── converters │ │ │ │ │ ├── percent.js │ │ │ │ │ └── any.js │ │ │ │ ├── resources │ │ │ │ │ ├── get-mappings.js │ │ │ │ │ └── update-mappings.js │ │ │ │ └── artifacts │ │ │ │ │ ├── update.js │ │ │ │ │ └── get.js │ │ │ ├── relationships.yaml │ │ │ ├── policies.yaml │ │ │ ├── groups.yaml │ │ │ ├── repositories.yaml │ │ │ ├── nodes.yaml │ │ │ ├── artifacts.yaml │ │ │ ├── profile.yaml │ │ │ └── interfaces.yaml │ │ └── README.md │ │ ├── mariadb │ │ ├── profile.yaml │ │ ├── nodes.yaml │ │ └── capabilities.yaml │ │ ├── ansible │ │ └── 1.0 │ │ │ ├── profile.yaml │ │ │ └── artifacts.yaml │ │ ├── helm │ │ └── 1.0 │ │ │ ├── artifacts.yaml │ │ │ ├── profile.yaml │ │ │ ├── nodes.yaml │ │ │ └── js │ │ │ └── kubernetes-resources-get.js │ │ ├── kubevirt │ │ └── 1.0 │ │ │ ├── profile.yaml │ │ │ ├── artifacts.yaml │ │ │ └── js │ │ │ └── kubernetes-resources-pre-get.js │ │ └── orchestration │ │ └── 1.0 │ │ ├── profile.yaml │ │ ├── js │ │ ├── states │ │ │ ├── reset.js │ │ │ ├── get.js │ │ │ └── set.js │ │ └── policies.js │ │ ├── data.yaml │ │ ├── artifacts.yaml │ │ ├── interfaces.yaml │ │ └── policies.yaml ├── kubernetes │ ├── cluster-mode-authorization.yaml │ ├── hello-world.yaml │ └── custom-resource-definition.yaml └── profile-generator │ └── kubevirt.yaml ├── NOTICE ├── apis ├── clientset │ └── versioned │ │ ├── fake │ │ ├── doc.go │ │ ├── register.go │ │ └── clientset_generated.go │ │ ├── scheme │ │ ├── doc.go │ │ └── register.go │ │ └── typed │ │ └── turandot.puccini.cloud │ │ └── v1alpha1 │ │ ├── fake │ │ ├── doc.go │ │ └── fake_turandot.puccini.cloud_client.go │ │ ├── generated_expansion.go │ │ └── doc.go ├── listers │ └── turandot.puccini.cloud │ │ └── v1alpha1 │ │ └── expansion_generated.go ├── informers │ └── externalversions │ │ ├── internalinterfaces │ │ └── factory_interfaces.go │ │ ├── turandot.puccini.cloud │ │ ├── v1alpha1 │ │ │ └── interface.go │ │ └── interface.go │ │ └── generic.go └── applyconfiguration │ ├── internal │ └── internal.go │ ├── turandot.puccini.cloud │ └── v1alpha1 │ │ ├── servicetemplate.go │ │ ├── servicetemplateindirect.go │ │ ├── servicenodemodestate.go │ │ ├── servicespec.go │ │ └── servicetemplatedirect.go │ └── utils.go ├── turandot-profile-generator ├── main.go ├── command.go ├── configuration.go ├── version.go ├── parse.go └── generator.go ├── controller ├── common.go ├── README.md ├── parser │ ├── outputs.go │ ├── kubernetes-artifact.go │ ├── orchestration-state.go │ ├── clout-attribute-value.go │ └── kubernetes-resource-mapping.go ├── tosca.go ├── policy.go ├── events.go ├── delegation.go ├── substitution.go ├── artifact.go └── puccini.go ├── turandot-operator ├── common.go ├── main.go └── controller.go ├── client ├── rest.go ├── tls.go └── registry.go └── .goreleaser.yml /scripts/header.go.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | turandot.puccini.cloud -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /lab/linphone/.gitignore: -------------------------------------------------------------------------------- 1 | linphonerc-edge 2 | -------------------------------------------------------------------------------- /examples/helm/artifacts/.gitignore: -------------------------------------------------------------------------------- 1 | charts/ 2 | -------------------------------------------------------------------------------- /lab/README.md: -------------------------------------------------------------------------------- 1 | Turandot Lab 2 | ============ 3 | -------------------------------------------------------------------------------- /turandot/README.md: -------------------------------------------------------------------------------- 1 | turandot 2 | -------- 3 | 4 | -------------------------------------------------------------------------------- /examples/self-contained/artifacts/.gitignore: -------------------------------------------------------------------------------- 1 | images/ 2 | -------------------------------------------------------------------------------- /lab/linphone/_env: -------------------------------------------------------------------------------- 1 | 2 | LIBVIRT_GATEWAY=192.168.39.1 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | go.work 3 | go.work.sum 4 | 5 | dist/ 6 | -------------------------------------------------------------------------------- /examples/telephony-network-service/artifacts/.gitignore: -------------------------------------------------------------------------------- 1 | images/ 2 | binaries/ 3 | -------------------------------------------------------------------------------- /resources/README.md: -------------------------------------------------------------------------------- 1 | 2 | The directory structure is: 3 | 4 | [group]/[version] 5 | -------------------------------------------------------------------------------- /assets/media/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tliron/turandot/HEAD/assets/media/download.png -------------------------------------------------------------------------------- /assets/media/telephony-network-service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tliron/turandot/HEAD/assets/media/telephony-network-service.png -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Turandot 2 | Copyright 2020-2024 Tal Liron 3 | 4 | --- 5 | 6 | "Kubernetes" and "K8s" are registered trademarks of The Linux Foundation. 7 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/1.0/js/converters/percent.js: -------------------------------------------------------------------------------- 1 | 2 | exports.convert = function(value) { 3 | return (value * 100) + '%'; 4 | }; 5 | -------------------------------------------------------------------------------- /lab/watch: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CONTEXT=${1:-central} 4 | 5 | watch --color "$(which turandot)" service list --colorize=force --context="$CONTEXT" 6 | -------------------------------------------------------------------------------- /resources/turandot.puccini.cloud/common.go: -------------------------------------------------------------------------------- 1 | package puccini 2 | 3 | // Kubernetes API group name (must have a ".") 4 | const GroupName = "turandot.puccini.cloud" 5 | -------------------------------------------------------------------------------- /apis/clientset/versioned/fake/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // This package has the automatically generated fake clientset. 4 | package fake 5 | -------------------------------------------------------------------------------- /assets/tosca/profiles/mariadb/profile.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | namespace: cloud.puccini.mariadb 4 | 5 | imports: 6 | 7 | - nodes.yaml 8 | -------------------------------------------------------------------------------- /assets/tosca/profiles/ansible/1.0/profile.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | namespace: cloud.puccini.ansible 4 | 5 | imports: 6 | 7 | - artifacts.yaml 8 | -------------------------------------------------------------------------------- /apis/clientset/versioned/scheme/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // This package contains the scheme of the automatically generated clientset. 4 | package scheme 5 | -------------------------------------------------------------------------------- /assets/tosca/profiles/ansible/1.0/artifacts.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | artifact_types: 4 | 5 | Playbook: 6 | description: >- 7 | Ansible playbook 8 | -------------------------------------------------------------------------------- /apis/clientset/versioned/typed/turandot.puccini.cloud/v1alpha1/fake/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // Package fake has the automatically generated clients. 4 | package fake 5 | -------------------------------------------------------------------------------- /apis/clientset/versioned/typed/turandot.puccini.cloud/v1alpha1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | type ServiceExpansion interface{} 6 | -------------------------------------------------------------------------------- /turandot-profile-generator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/tliron/kutil/util" 5 | ) 6 | 7 | func main() { 8 | err := command.Execute() 9 | util.FailOnError(err) 10 | } 11 | -------------------------------------------------------------------------------- /apis/clientset/versioned/typed/turandot.puccini.cloud/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | // This package has the automatically generated typed clients. 4 | package v1alpha1 5 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | * [Hello World](hello-world/) 5 | * [Self-Contained](self-contained/) 6 | * [Helm](helm/) 7 | * [Telephony Network Service (MANO)](telephony-network-service/) 8 | -------------------------------------------------------------------------------- /examples/telephony-network-service/artifacts/asterisk/rtp.conf: -------------------------------------------------------------------------------- 1 | ; https://www.voip-info.org/asterisk-config-rtpconf/ 2 | 3 | [general] 4 | rtpstart=10000 5 | rtpend=10007 6 | ;rtpchecksums=no 7 | ;strictrtp=no 8 | -------------------------------------------------------------------------------- /lab/openshift/shell: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../scripts/_env" 6 | 7 | NODE=$(oc get node --output name) 8 | oc debug "$NODE" 9 | -------------------------------------------------------------------------------- /turandot/commands/version.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/tliron/kutil/cobra" 5 | ) 6 | 7 | func init() { 8 | rootCommand.AddCommand(cobra.NewVersionCommand(toolName)) 9 | } 10 | -------------------------------------------------------------------------------- /assets/tosca/profiles/helm/1.0/artifacts.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | artifact_types: 4 | 5 | Chart: 6 | description: >- 7 | Helm chart. 8 | file_ext: [ tar, tar.gz, tgz ] 9 | -------------------------------------------------------------------------------- /scripts/logs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | 7 | POD=$(kubectl_first_pod puccini-kubernetes-operator) 8 | kubectl logs "$POD" --namespace="$WORKSPACE" "$@" 9 | -------------------------------------------------------------------------------- /resources/turandot.puccini.cloud/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | // Note: Generator *requires* file to be called "doc.go" 4 | 5 | // +k8s:deepcopy-gen=package 6 | // +groupName=turandot.puccini.cloud 7 | const Version = "v1alpha1" 8 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/1.0/relationships.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | imports: 4 | 5 | - capabilities.yaml 6 | 7 | relationship_types: 8 | 9 | Route: 10 | valid_target_types: [ Service ] # capability 11 | -------------------------------------------------------------------------------- /controller/common.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | const ( 4 | NamePrefix = "turandot" 5 | PartOf = "Turandot" 6 | ManagedBy = "Turandot" 7 | OperatorImageName = "tliron/turandot-operator" 8 | CacheDirectory = "/cache" 9 | ) 10 | -------------------------------------------------------------------------------- /scripts/all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | . "$HERE/_trap" 7 | 8 | "$HERE/build-container-image" 9 | "$HERE/publish-container-image" 10 | "$HERE/deploy-operator" -c 11 | "$HERE/logs" -f 12 | -------------------------------------------------------------------------------- /turandot-operator/common.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | contextpkg "context" 5 | 6 | "github.com/tliron/commonlog" 7 | ) 8 | 9 | const toolName = "turandot-operator" 10 | 11 | var context = contextpkg.TODO() 12 | 13 | var log = commonlog.GetLogger(toolName) 14 | -------------------------------------------------------------------------------- /scripts/push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | 7 | FROM=$1 8 | TO=$2 9 | 10 | POD=$(kubectl_first_pod puccini-kubernetes-operator) 11 | 12 | kubectl cp "$FROM" "$POD:$TO" --namespace="$WORKSPACE" 13 | -------------------------------------------------------------------------------- /turandot/commands/service.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | rootCommand.AddCommand(serviceCommand) 9 | } 10 | 11 | var serviceCommand = &cobra.Command{ 12 | Use: "service", 13 | Short: "Work with services", 14 | } 15 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/1.0/js/converters/any.js: -------------------------------------------------------------------------------- 1 | 2 | exports.convert = function(value) { 3 | for (let name in value) { 4 | let field = value[name]; 5 | if (field !== undefined) { 6 | return field; 7 | } 8 | } 9 | return null; 10 | }; 11 | -------------------------------------------------------------------------------- /turandot/commands/delegate.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | rootCommand.AddCommand(delegateCommand) 9 | } 10 | 11 | var delegateCommand = &cobra.Command{ 12 | Use: "delegate", 13 | Short: "Work with delegates", 14 | } 15 | -------------------------------------------------------------------------------- /turandot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/tliron/kutil/util" 5 | "github.com/tliron/turandot/turandot/commands" 6 | 7 | _ "github.com/tliron/commonlog/simple" 8 | ) 9 | 10 | func main() { 11 | util.ExitOnSignals() 12 | commands.Execute() 13 | util.Exit(0) 14 | } 15 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/1.0/policies.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | # https://istio.io/docs/reference/config/istio.networking.v1alpha3.html 4 | 5 | policy_types: 6 | 7 | TrafficPolicy: 8 | description: >- 9 | DestinationRule (with TrafficPolicy) 10 | -------------------------------------------------------------------------------- /turandot/commands/operator.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | rootCommand.AddCommand(operatorCommand) 9 | } 10 | 11 | var operatorCommand = &cobra.Command{ 12 | Use: "operator", 13 | Short: "Control the Turandot operator", 14 | } 15 | -------------------------------------------------------------------------------- /turandot/commands/template.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | rootCommand.AddCommand(templateCommand) 9 | } 10 | 11 | var templateCommand = &cobra.Command{ 12 | Use: "template", 13 | Short: "Work with service templates in the repository", 14 | } 15 | -------------------------------------------------------------------------------- /examples/build-csars: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../scripts/_env" 6 | 7 | "$HERE/hello-world/scripts/build-csar" 8 | "$HERE/self-contained/scripts/build-csar" 9 | "$HERE/helm/scripts/build-csar" 10 | "$HERE/telephony-network-service/scripts/build-csars" 11 | -------------------------------------------------------------------------------- /turandot-operator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/tliron/kutil/util" 5 | 6 | _ "github.com/tliron/commonlog/simple" 7 | _ "k8s.io/client-go/plugin/pkg/client/auth" // load all auth plugins 8 | ) 9 | 10 | func main() { 11 | err := command.Execute() 12 | util.FailOnError(err) 13 | util.Exit(0) 14 | } 15 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubevirt/1.0/profile.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | metadata: 4 | 5 | puccini.scriptlet.import:kubernetes.plugins.resources.pre-get.kubevirt: js/kubernetes-resources-pre-get.js 6 | 7 | namespace: cloud.puccini.kubevirt 8 | 9 | imports: 10 | 11 | - capabilities.yaml 12 | - artifacts.yaml 13 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | . "$HERE/_trap" 7 | 8 | "$HERE/build" 9 | 10 | # -count=1 is the idiomatic way to disable test caching 11 | 12 | m 'testing...' 13 | 14 | #ROOT=$ROOT \ 15 | #go test -count=1 github.com/tliron/puccini/puccini-tosca "$@" 16 | -------------------------------------------------------------------------------- /scripts/refresh-mod: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | . "$HERE/_trap" 7 | 8 | rm --force "$ROOT/go.mod" "$ROOT/go.sum" 9 | 10 | cd "$ROOT" 11 | go mod init "$MODULE" 12 | go get k8s.io/client-go@v$K8S_VERSION 13 | go mod tidy 14 | 15 | "$HERE/test" 16 | 17 | go mod tidy 18 | -------------------------------------------------------------------------------- /assets/tosca/profiles/helm/1.0/profile.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | metadata: 4 | 5 | puccini.scriptlet.import:kubernetes.plugins.resources.get.helm: js/kubernetes-resources-get.js 6 | 7 | namespace: cloud.puccini.helm 8 | 9 | imports: 10 | 11 | - nodes.yaml 12 | - artifacts.yaml 13 | - ../../kubernetes/1.0/profile.yaml 14 | -------------------------------------------------------------------------------- /examples/hello-world/README.md: -------------------------------------------------------------------------------- 1 | Hello World Example 2 | =================== 3 | 4 | A stateless single-pod web workload comprising a deployment and a loadbalancer service. 5 | 6 | Follow the [tutorial](../../TUTORIAL.md) for complete instructions on how to deploy it. 7 | 8 | 9 | Helper Scripts 10 | -------------- 11 | 12 | * [Package as CSAR file](scripts/build-csar) 13 | -------------------------------------------------------------------------------- /scripts/generate-profiles: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | . "$HERE/_trap" 7 | 8 | go install "$ROOT/turandot-profile-generator" 9 | 10 | turandot-profile-generator "$ROOT/assets/profile-generator/kubernetes.yaml" 11 | turandot-profile-generator "$ROOT/assets/profile-generator/kubevirt.yaml" 12 | -------------------------------------------------------------------------------- /assets/kubernetes/cluster-mode-authorization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | 4 | metadata: 5 | name: turandot 6 | 7 | subjects: 8 | - kind: ServiceAccount 9 | name: turandot 10 | namespace: !!string $NAMESPACE # required 11 | 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: cluster-admin 16 | -------------------------------------------------------------------------------- /examples/hello-world/artifacts/scripts/configure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ -f /tmp/configured ]; then 5 | exit 0 6 | fi 7 | 8 | NODE_TEMPLATE=$1 9 | 10 | if ! grep -qF TOSCA /usr/src/app/views/home.handlebars; then 11 | echo "
Part of TOSCA node: \"$NODE_TEMPLATE\"
" >> /usr/src/app/views/home.handlebars 12 | fi 13 | 14 | touch /tmp/configured 15 | -------------------------------------------------------------------------------- /examples/self-contained/artifacts/scripts/configure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ -f /tmp/configured ]; then 5 | exit 0 6 | fi 7 | 8 | NODE_TEMPLATE=$1 9 | 10 | if ! grep -qF TOSCA /usr/src/app/views/home.handlebars; then 11 | echo "
Part of TOSCA node: \"$NODE_TEMPLATE\"
" >> /usr/src/app/views/home.handlebars 12 | fi 13 | 14 | touch /tmp/configured 15 | -------------------------------------------------------------------------------- /turandot/commands/operator-status.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | operatorCommand.AddCommand(operatorStatusCommand) 9 | } 10 | 11 | var operatorStatusCommand = &cobra.Command{ 12 | Use: "status", 13 | Short: "Show the status of the Turandot operator", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /lab/minikube/prepare: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | minikube profile central 9 | 10 | "$HERE/../cert-manager/deploy" 11 | "$HERE/../multus/deploy" 12 | "$HERE/../kubevirt/deploy" 13 | 14 | kubectl create namespace workspace || true 15 | 16 | "$HERE/../mariadb/deploy" 17 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/1.0/groups.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | imports: 4 | 5 | - nodes.yaml 6 | 7 | group_types: 8 | 9 | Namespace: 10 | description: >- 11 | Will automatically use a "group" label (the name of the group) for all deployment controllers. 12 | members: 13 | - Service 14 | properties: 15 | namespace: 16 | type: string 17 | -------------------------------------------------------------------------------- /examples/telephony-network-service/scripts/ssh-vnf: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | 6 | KEY=$HERE/../artifacts/keypairs/admin@asterisk-vnf 7 | 8 | IP=$(turandot service output asterisk-vnf tcp-ip) 9 | 10 | ssh \ 11 | -i "$KEY" \ 12 | -o StrictHostKeyChecking=no \ 13 | -o UserKnownHostsFile=/dev/null \ 14 | -o LogLevel=ERROR \ 15 | "admin@$IP" \ 16 | "$@" 17 | -------------------------------------------------------------------------------- /turandot/commands/operator-shell.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | operatorCommand.AddCommand(operatorShellCommand) 9 | } 10 | 11 | var operatorShellCommand = &cobra.Command{ 12 | Use: "shell", 13 | Short: "Opens a shell to the Turandot operator", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | Shell("operator", "operator") 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /examples/helm/hello-world/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "hello-world.serviceAccountName" . }} 6 | labels: 7 | {{- include "hello-world.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /lab/linphone/linphone-central: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../scripts/_env" 6 | . "$HERE/_env" 7 | 8 | minikube profile central 9 | 10 | EXTERNAL_IP=$(turandot service output asterisk-vnf udp-ip -n workspace) 11 | 12 | cat "$HERE/linphonerc-central.template" | \ 13 | IP=$EXTERNAL_IP NAT_IP=$LIBVIRT_GATEWAY envsubst > ~/.linphonerc 14 | 15 | linphone 16 | -------------------------------------------------------------------------------- /apis/listers/turandot.puccini.cloud/v1alpha1/expansion_generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by lister-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | // ServiceListerExpansion allows custom methods to be added to 6 | // ServiceLister. 7 | type ServiceListerExpansion interface{} 8 | 9 | // ServiceNamespaceListerExpansion allows custom methods to be added to 10 | // ServiceNamespaceLister. 11 | type ServiceNamespaceListerExpansion interface{} 12 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubevirt/1.0/artifacts.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | artifact_types: 4 | 5 | CloudConfig: 6 | description: >- 7 | Configuration for cloud-init 8 | file_ext: [ yaml, yml ] 9 | properties: 10 | base64: 11 | type: boolean 12 | default: false 13 | variables: 14 | type: map 15 | entry_schema: string 16 | required: false 17 | -------------------------------------------------------------------------------- /examples/hello-world/artifacts/scriptlets/set-output.js: -------------------------------------------------------------------------------- 1 | 2 | const tosca = require('tosca.lib.utils'); 3 | 4 | let nodeTemplateName = puccini.arguments.nodeTemplate; 5 | let name = puccini.arguments.name; 6 | let value = puccini.arguments.value; 7 | 8 | puccini.log.infof('execution for node template %q, setting output: %s -> %s', nodeTemplateName, name, value); 9 | 10 | if (tosca.setOutputValue(name, value)) 11 | puccini.write(clout); 12 | -------------------------------------------------------------------------------- /lab/openshift/prepare: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | minikube profile edge 9 | 10 | "$HERE/../multus/deploy" 11 | 12 | oc config use-context crc-admin 13 | 14 | "$HERE/../multus/deploy" 15 | "$HERE/../kubevirt/deploy" 16 | 17 | kubectl create namespace workspace || true 18 | 19 | "$HERE/../mariadb/deploy" 20 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/1.0/js/resources/get-mappings.js: -------------------------------------------------------------------------------- 1 | 2 | let mappings = {}; 3 | 4 | for (let vertexId in clout.vertexes) { 5 | let vertex = clout.vertexes[vertexId]; 6 | if (!vertex.metadata.turandot || 7 | (vertex.metadata.turandot.version !== '1.0')) 8 | continue; 9 | 10 | if (vertex.metadata.turandot.resources) 11 | mappings[vertexId] = vertex.metadata.turandot.resources; 12 | } 13 | 14 | puccini.write(mappings); 15 | -------------------------------------------------------------------------------- /examples/helm/scripts/build-chart: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | CHARTS=$(readlink --canonicalize "$HERE/../artifacts/charts") 9 | 10 | mkdir --parents "$CHARTS" 11 | 12 | rm --force "$CHARTS/hello-world.tar.gz" 13 | tar --create --gzip --file="$CHARTS/hello-world.tar.gz" --directory="$HERE/../hello-world" . 14 | -------------------------------------------------------------------------------- /examples/self-contained/artifacts/scriptlets/set-output.js: -------------------------------------------------------------------------------- 1 | 2 | const tosca = require('tosca.lib.utils'); 3 | 4 | let nodeTemplateName = puccini.arguments.nodeTemplate; 5 | let name = puccini.arguments.name; 6 | let value = puccini.arguments.value; 7 | 8 | puccini.log.infof('execution for node template %q, setting output: %s -> %s', nodeTemplateName, name, value); 9 | 10 | if (tosca.setOutputValue(name, value)) 11 | puccini.write(clout); 12 | -------------------------------------------------------------------------------- /scripts/format: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | 7 | gofmt -w -s -e \ 8 | "$ROOT/client" \ 9 | "$ROOT/controller" \ 10 | "$ROOT/controller/parser" \ 11 | "$ROOT/resources" \ 12 | "$ROOT/resources/turandot.puccini.cloud" \ 13 | "$ROOT/resources/turandot.puccini.cloud/v1alpha1" \ 14 | "$ROOT/turandot" \ 15 | "$ROOT/turandot/commands" \ 16 | "$ROOT/turandot-operator" -------------------------------------------------------------------------------- /scripts/nuke-namespace: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | 7 | # https://medium.com/@clouddev.guru/how-to-fix-kubernetes-namespace-deleting-stuck-in-terminating-state-5ed75792647e 8 | 9 | NAMESPACE=$1 10 | 11 | kubectl get namespaces "$NAMESPACE" --output=json | \ 12 | jq .spec.finalizers=[] | \ 13 | kubectl replace --raw="/api/v1/namespaces/$NAMESPACE/finalize" -f - 14 | -------------------------------------------------------------------------------- /controller/README.md: -------------------------------------------------------------------------------- 1 | https://github.com/kubernetes/sample-controller 2 | https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/sample-controller 3 | https://github.com/kubernetes/community/blob/8cafef897a22026d42f5e5bb3f104febe7e29830/contributors/devel/controllers.md 4 | https://engineering.bitnami.com/articles/a-deep-dive-into-kubernetes-controllers.html 5 | https://github.com/operator-framework/community-operators/blob/master/docs/best-practices.md 6 | -------------------------------------------------------------------------------- /lab/linphone/linphonec-edge: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../scripts/_env" 6 | . "$HERE/_env" 7 | 8 | minikube profile edge 9 | 10 | EXTERNAL_IP=$(turandot service output asterisk-cnf udp-ip -n workspace) 11 | 12 | cat "$HERE/linphonerc-edge.template" | \ 13 | IP=$EXTERNAL_IP NAT_IP=$LIBVIRT_GATEWAY envsubst > "$HERE/linphonerc-edge" 14 | 15 | linphonec -c "$HERE/linphonerc-edge" 16 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/1.0/repositories.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | repositories: 4 | 5 | docker-hub: 6 | description: >- 7 | Docker Hub 8 | url: docker.io 9 | 10 | quay: 11 | description: >- 12 | Quay 13 | url: quay.io 14 | 15 | red-hat: 16 | url: registry.redhat.io 17 | 18 | centos: 19 | url: registry.centos.org 20 | 21 | fedora: 22 | url: registry.fedoraproject.org 23 | -------------------------------------------------------------------------------- /lab/configure: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | cd "$ROOT/../telephony-network-service" 9 | 10 | minikube profile central 11 | 12 | EXTERNAL_IP=$(kubectl_external_ip asterisk-vnf-tcp) 13 | IP=$EXTERNAL_IP \ 14 | ssh/update 15 | 16 | data-plane/configure 17 | asterisk/vnf/configure 18 | asterisk/cnf/configure 19 | 20 | minikube profile central 21 | -------------------------------------------------------------------------------- /examples/helm/hello-world/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "hello-world.fullname" . }} 5 | labels: 6 | {{- include "hello-world.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "hello-world.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /examples/helm/hello-world/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /scripts/install-bash-completion: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # See: https://github.com/scop/bash-completion/blob/master/README.md 5 | 6 | USER_DIR=$BASH_COMPLETION_USER_DIR 7 | 8 | if [ -z "$USER_DIR" ]; then 9 | DATA_HOME=$XDG_DATA_HOME 10 | if [ -z "$DATA_HOME" ]; then 11 | DATA_HOME=~/.local/share 12 | fi 13 | USER_DIR=$DATA_HOME/bash-completion 14 | fi 15 | 16 | mkdir --parents "$USER_DIR/completions" 17 | turandot completion bash > "$USER_DIR/completions/turandot" 18 | -------------------------------------------------------------------------------- /examples/telephony-network-service/scripts/register-csars: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | function register () { 9 | local NAME=$1 10 | turandot template register "$NAME" --file="$ROOT/dist/$NAME.csar" --namespace=workspace 11 | } 12 | 13 | register telephony-network-service 14 | register asterisk-cnf 15 | register asterisk-vnf 16 | register simple-data-plane 17 | -------------------------------------------------------------------------------- /examples/telephony-network-service/artifacts/asterisk/extensions-cnf.conf.template: -------------------------------------------------------------------------------- 1 | [default] 2 | 3 | exten => _X.,1,Log(NOTICE, $__{CALLERID(num)} called $__{EXTEN}) 4 | same => n,Dial(PJSIP/$__{EXTEN}) 5 | same => n,Dial(PJSIP/trunk/sip:$__{EXTEN}@$TRUNK_IP) 6 | same => n,Answer() 7 | same => n,Playback(im-sorry) 8 | same => n,Playback(extension) 9 | same => n,SayDigits($__{EXTEN}) 10 | same => n,Playback(vm-isunavail) 11 | same => n,Playback(cannot-complete-as-dialed) 12 | same => n,Hangup() 13 | -------------------------------------------------------------------------------- /scripts/pull: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | 7 | FROM=$1 8 | TO=$2 9 | 10 | if [ -z "$TO" ]; then 11 | TO=$(basename "$FROM") 12 | elif [ -d "$TO" ]; then 13 | TO=$TO/$(basename "$FROM") 14 | elif [ "${TO: -1}" == / ]; then 15 | mkdir --parents "$TO" 16 | TO=$TO$(basename "$FROM") 17 | fi 18 | 19 | POD=$(kubectl_first_pod puccini-kubernetes-operator) 20 | 21 | kubectl cp "$POD:$FROM" "$TO" --namespace="$WORKSPACE" 22 | -------------------------------------------------------------------------------- /lab/linphone/linphonerc-central.template: -------------------------------------------------------------------------------- 1 | [net] 2 | firewall_policy=1 3 | nat_address=$NAT_IP 4 | 5 | [sip] 6 | sip_port=5063 7 | default_proxy=0 8 | 9 | [rtp] 10 | audio_rtp_port=7081 11 | video_rtp_port=9081 12 | 13 | [proxy_0] 14 | reg_identity=sip:201@$IP 15 | reg_proxy= 16 | reg_sendregister=1 17 | 18 | [auth_info_0] 19 | username=201 20 | passwd=password 21 | 22 | [sound] 23 | playback_dev_id=PulseAudio: default 24 | ringer_dev_id=PulseAudio: default 25 | capture_dev_id=PulseAudio: default 26 | -------------------------------------------------------------------------------- /lab/linphone/linphonerc-edge.template: -------------------------------------------------------------------------------- 1 | [net] 2 | firewall_policy=1 3 | nat_address=$NAT_IP 4 | 5 | [sip] 6 | sip_port=5061 7 | default_proxy=0 8 | 9 | [rtp] 10 | audio_rtp_port=7079 11 | video_rtp_port=9079 12 | 13 | [proxy_0] 14 | reg_identity=sip:100@$IP 15 | reg_proxy= 16 | reg_sendregister=1 17 | 18 | [auth_info_0] 19 | username=100 20 | passwd=password 21 | 22 | [sound] 23 | playback_dev_id=PulseAudio: default 24 | ringer_dev_id=PulseAudio: default 25 | capture_dev_id=PulseAudio: default 26 | -------------------------------------------------------------------------------- /lab/minikube/prepare-multi: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | minikube profile edge 9 | 10 | "$HERE/../cert-manager/deploy" 11 | "$HERE/../multus/deploy" 12 | 13 | minikube profile central 14 | 15 | "$HERE/../cert-manager/deploy" 16 | "$HERE/../multus/deploy" 17 | "$HERE/../kubevirt/deploy" 18 | 19 | kubectl create namespace workspace || true 20 | 21 | "$HERE/../mariadb/deploy" 22 | -------------------------------------------------------------------------------- /examples/helm/hello-world/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "hello-world.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "hello-world.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "hello-world.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /turandot-profile-generator/command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/tliron/kutil/util" 6 | ) 7 | 8 | var command = &cobra.Command{ 9 | Use: "turandot-profile-generator [CONFIGURATION PATH]", 10 | Short: "Turandot Profile Generator", 11 | Args: cobra.ExactArgs(1), 12 | Run: func(cmd *cobra.Command, args []string) { 13 | generator, err := NewGenerator(args[0]) 14 | util.FailOnError(err) 15 | 16 | err = generator.Generate() 17 | util.FailOnError(err) 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /examples/self-contained/scripts/save-container-image: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | # See: https://github.com/paulbouwer/hello-kubernetes 9 | 10 | IMAGE=docker.io/paulbouwer/hello-kubernetes:1.8 11 | IMAGES=$(readlink --canonicalize "$HERE/../artifacts/images") 12 | 13 | mkdir --parents "$IMAGES" 14 | 15 | podman pull "$IMAGE" 16 | "$ROOT/scripts/save-portable-container-image" "$IMAGE" "$IMAGES/hello-world.tar.gz" 17 | -------------------------------------------------------------------------------- /scripts/_env: -------------------------------------------------------------------------------- 1 | 2 | _HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 3 | 4 | . "$_HERE/_functions" 5 | 6 | MODULE=github.com/tliron/turandot 7 | 8 | K8S_VERSION=0.27.3 9 | K8S_API_VERSION=1.27 10 | HELM_VERSION=3.12.1 11 | YQ_VERSION=4.34.1 12 | 13 | ROOT=$(readlink --canonicalize "$_HERE/..") 14 | 15 | GOPATH=${GOPATH:-$HOME/go} 16 | export PATH=$GOPATH/bin:$PATH 17 | 18 | WORKSPACE=${WORKSPACE:-workspace} 19 | 20 | if [ -d /Depot/Temporary ]; then 21 | export TMPDIR=/Depot/Temporary 22 | else 23 | export TMPDIR=/tmp 24 | fi 25 | -------------------------------------------------------------------------------- /scripts/install-tools: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ "$EUID" -ne 0 ]; then 5 | echo "Run this script as root" 6 | exit 1 7 | fi 8 | 9 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 10 | . "$HERE/_env" 11 | 12 | FORCE=false 13 | if [ "$1" == -f ]; then 14 | FORCE=true 15 | fi 16 | 17 | install_tool helm "$HELM_VERSION" \ 18 | "https://get.helm.sh/helm-v$HELM_VERSION-linux-amd64.tar.gz" 1 \ 19 | "linux-amd64/" 20 | 21 | install_tool yq "$YQ_VERSION" \ 22 | "https://github.com/mikefarah/yq/releases/download/v$YQ_VERSION/yq_linux_amd64" 23 | -------------------------------------------------------------------------------- /turandot/commands/operator-uninstall.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | operatorCommand.AddCommand(operatorUninstallCommand) 9 | operatorUninstallCommand.Flags().BoolVarP(&wait, "wait", "w", false, "wait for uninstallation to succeed") 10 | } 11 | 12 | var operatorUninstallCommand = &cobra.Command{ 13 | Use: "uninstall", 14 | Short: "Uninstall the Turandot operator", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | NewClient().Turandot().UninstallOperator(wait) 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /assets/tosca/profiles/orchestration/1.0/profile.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | metadata: 4 | 5 | puccini.scriptlet.import:orchestration.policies: js/policies.js 6 | puccini.scriptlet.import:orchestration.states.get: js/states/get.js 7 | puccini.scriptlet.import:orchestration.states.set: js/states/set.js 8 | puccini.scriptlet.import:orchestration.states.reset: js/states/reset.js 9 | 10 | namespace: cloud.puccini.turandot.orchestration 11 | 12 | imports: 13 | 14 | - policies.yaml 15 | - interfaces.yaml 16 | - artifacts.yaml 17 | -------------------------------------------------------------------------------- /examples/self-contained/README.md: -------------------------------------------------------------------------------- 1 | Self-Contained Example 2 | ====================== 3 | 4 | A stateless single-pod web workload comprising a deployment and a loadbalancer service. 5 | 6 | This example demonstrates how a container image can be included as an artifact within a CSAR. 7 | 8 | Follow the [tutorial](../../TUTORIAL-SELF-CONTAINED.md) for complete instructions 9 | on how to deploy it. 10 | 11 | 12 | Helper Scripts 13 | -------------- 14 | 15 | * [Save container images as tarball](scripts/save-container-image) 16 | * [Package as CSAR file](scripts/build-csar) 17 | -------------------------------------------------------------------------------- /scripts/delete: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | . "$HERE/_trap" 7 | 8 | export VERSION=1.0 9 | 10 | m "deleting operator from namespace \"$WORKSPACE\"..." 11 | 12 | kubectl_delete_template "$ROOT/assets/hello-world.yaml" 13 | kubectl_delete_template "$ROOT/assets/turandot.yaml" 14 | kubectl_delete_template "$ROOT/assets/cluster-mode-authorization.yaml" 15 | kubectl_delete_template "$ROOT/assets/namespace.yaml" 16 | kubectl_delete_template "$ROOT/assets/custom-resource-definitions.yaml" 17 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/1.0/js/resources/update-mappings.js: -------------------------------------------------------------------------------- 1 | 2 | const tosca = require('tosca.lib.utils'); 3 | 4 | for (let vertexId in puccini.arguments) { 5 | let mappings = puccini.arguments[vertexId]; 6 | let vertex = clout.vertexes[vertexId]; 7 | if (vertex === undefined) 8 | continue; 9 | 10 | if (!vertex.metadata.turandot) 11 | vertex.metadata.turandot = {}; 12 | vertex.metadata.turandot.version = '1.0'; 13 | vertex.metadata.turandot.resources = JSON.parse(mappings); 14 | } 15 | 16 | tosca.addHistory('kubernetes.resources.update-mappings'); 17 | puccini.write(clout); 18 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/1.0/nodes.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | imports: 4 | 5 | - capabilities.yaml 6 | - relationships.yaml 7 | 8 | node_types: 9 | 10 | Service: 11 | description: >- 12 | Represents a microservice: a controlled pod (Deployment) plus a controlled endpoint (Service). 13 | capabilities: 14 | metadata: Metadata 15 | service: Service 16 | deployment: Deployment 17 | requirements: 18 | - route: 19 | capability: Service 20 | relationship: Route 21 | occurrences: [ 0, UNBOUNDED ] 22 | -------------------------------------------------------------------------------- /scripts/publish-container-image: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | . "$HERE/_trap" 7 | 8 | m 'Login to docker.io' 9 | buildah login docker.io 10 | 11 | function push () { 12 | local EXECUTABLE=$1 13 | local IMAGE=$EXECUTABLE 14 | local LOCAL=localhost/$IMAGE 15 | local REMOTE=docker://docker.io/tliron/$IMAGE 16 | #local REMOTE=docker://$(minikube ip):5000/tliron/$IMAGE 17 | 18 | skopeo delete --tls-verify=false "$REMOTE" || true 19 | buildah push --tls-verify=false "$LOCAL" "$REMOTE" 20 | } 21 | 22 | push turandot-operator 23 | -------------------------------------------------------------------------------- /examples/telephony-network-service/artifacts/keypairs/admin@asterisk-vnf.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDpV4zIqJNcD905igP/Ei4nwo8+LCdIGgCYUEMBjo6JITzIhVRKhpwjvYW28DJQ4mvfRHkyyGlgy2JKbrOXLYyqoFzxE99VRi+dwI0UWvQ16fRIhOYC7X103ByYfxrppl2L275t9DxYHvbrlMfLM490EKG/dWHeyHyf5SU045S6rEZXpdJUnD5CPrFSfAxeumpcHGqTN8FrzWcfX947sAOETCHVPuTMQlCvWHMtrseGMiED6M5eFrhAovwnNrrSRsCWSEk4jyHNY06SQIavoesyemahGxIXBC7SbiFXyXo5AQ5HTwbLNMcqHha0pv5oZt8QKKddRD3xhTNh1McH2mdawC1NsFhoiuKWNCghwHmR/EHN01sMKx21mJ24+HY+GGyRbxgTywfEJz5Q00JZ3rF4Kz0X4Endr098JWeC7Q5j/vpkz2gblGV6i9vkMwBr1yY0DbBI8QaI5BM6g2Ph+IFUNrL1LVWYsUzZepJMk6YTUs6+DhxxS5D3m/fnTLczxZs= emblemparade@foucault 2 | -------------------------------------------------------------------------------- /assets/kubernetes/hello-world.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: turandot.puccini.cloud/v1alpha1 3 | kind: Service 4 | 5 | metadata: 6 | name: hello-world 7 | namespace: !!string $NAMESPACE 8 | 9 | labels: 10 | app.kubernetes.io/name: hello-world-service 11 | app.kubernetes.io/instance: hello-world-service-$NAMESPACE 12 | app.kubernetes.io/version: !!string $VERSION 13 | app.kubernetes.io/component: service 14 | app.kubernetes.io/part-of: hello-world 15 | app.kubernetes.io/managed-by: turandot 16 | 17 | spec: 18 | serviceTemplateUrl: /cache/bookinfo.csar 19 | inputs: 20 | v1: hello 21 | v2: world 22 | v3: '12' 23 | -------------------------------------------------------------------------------- /turandot/commands/operator-logs.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | operatorCommand.AddCommand(operatorLogsCommand) 9 | operatorLogsCommand.Flags().IntVarP(&tail, "tail", "t", -1, "number of most recent lines to print (<0 means all lines)") 10 | operatorLogsCommand.Flags().BoolVarP(&follow, "follow", "f", false, "keep printing incoming logs") 11 | } 12 | 13 | var operatorLogsCommand = &cobra.Command{ 14 | Use: "logs", 15 | Short: "Show the logs of the Turandot operator", 16 | Run: func(cmd *cobra.Command, args []string) { 17 | Logs("operator", "operator") 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /controller/parser/outputs.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tliron/go-ard" 7 | cloutpkg "github.com/tliron/puccini/clout" 8 | ) 9 | 10 | func GetOutputs(clout *cloutpkg.Clout) (map[string]string, bool) { 11 | if tosca, ok := clout.Properties["tosca"]; ok { 12 | if outputs, ok := ard.With(tosca).Get("outputs").NilMeansZero().StringMap(); ok { 13 | outputs_ := make(map[string]string) 14 | for name, output := range outputs { 15 | outputs_[name] = fmt.Sprintf("%v", output) 16 | } 17 | return outputs_, true 18 | } else { 19 | return nil, false 20 | } 21 | } else { 22 | return nil, false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lab/openshift/README.md: -------------------------------------------------------------------------------- 1 | Red Hat CodeReady Containers 2 | ============================ 3 | 4 | https://code-ready.github.io/crc/ 5 | 6 | https://cloud.redhat.com/openshift/install/crc/installer-provisioned 7 | 8 | https://github.com/code-ready/crc 9 | 10 | To access the cluster, first set up your environment by following 'crc oc-env' instructions. 11 | Then you can access it by running 'oc login -u developer -p developer https://api.crc.testing:6443'. 12 | To login as an admin, run 'oc login -u kubeadmin -p duduw-yPT9Z-hsUpq-f3pre https://api.crc.testing:6443'. 13 | To access the cluster, first set up your environment by following 'crc oc-env' instructions. 14 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/1.0/js/artifacts/update.js: -------------------------------------------------------------------------------- 1 | 2 | const tosca = require('tosca.lib.utils'); 3 | 4 | for (let vertexId in clout.vertexes) { 5 | let vertex = clout.vertexes[vertexId]; 6 | if (!tosca.isNodeTemplate(vertex)) 7 | continue; 8 | let nodeTemplate = vertex.properties; 9 | 10 | for (let artifactName in nodeTemplate.artifacts) { 11 | let artifact = nodeTemplate.artifacts[artifactName]; 12 | if (artifact.sourcePath) { 13 | let url = puccini.arguments[artifact.sourcePath]; 14 | if (url !== undefined) 15 | artifact.$artifact = url; 16 | } 17 | } 18 | } 19 | 20 | tosca.addHistory('kubernetes.artifacts.update'); 21 | puccini.write(clout); 22 | -------------------------------------------------------------------------------- /assets/tosca/profiles/orchestration/1.0/js/states/reset.js: -------------------------------------------------------------------------------- 1 | 2 | const tosca = require('tosca.lib.utils'); 3 | 4 | let serviceName = puccini.arguments.service; 5 | let mode = puccini.arguments.mode; 6 | 7 | for (let vertexId in clout.vertexes) { 8 | let vertex = clout.vertexes[vertexId]; 9 | if (!tosca.isNodeTemplate(vertex)) 10 | continue; 11 | if (!vertex.metadata.turandot) 12 | continue; 13 | if (!vertex.metadata.turandot.states) 14 | continue; 15 | 16 | let state = vertex.metadata.turandot.states[serviceName]; 17 | if ((state !== undefined) && (state.mode === mode)) 18 | delete vertex.metadata.turandot.states[serviceName]; 19 | } 20 | 21 | puccini.write(clout); 22 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | . "$HERE/_trap" 7 | 8 | git_version 9 | 10 | function build () { 11 | local TOOL=$1 12 | pushd "$ROOT/$TOOL" > /dev/null 13 | go install \ 14 | -ldflags " \ 15 | -X 'github.com/tliron/kutil/version.GitVersion=$VERSION' \ 16 | -X 'github.com/tliron/kutil/version.GitRevision=$REVISION' \ 17 | -X 'github.com/tliron/kutil/version.Timestamp=$TIMESTAMP'" 18 | popd > /dev/null 19 | m "built $GOPATH/bin/$TOOL" 20 | } 21 | 22 | build turandot-operator 23 | build turandot 24 | 25 | ln --symbolic --force "$GOPATH/bin/turandot" "$GOPATH/bin/kubectl-tosca" 26 | -------------------------------------------------------------------------------- /client/rest.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "io" 5 | 6 | kubernetesutil "github.com/tliron/kutil/kubernetes" 7 | ) 8 | 9 | func (self *Client) WriteToContainer(namespace string, podName string, containerName string, reader io.Reader, targetPath string, permissions *int64) error { 10 | return kubernetesutil.WriteToContainer(self.REST, self.Config, namespace, podName, containerName, reader, targetPath, permissions) 11 | } 12 | 13 | func (self *Client) Exec(namespace string, podName string, containerName string, stdin io.Reader, stdout io.Writer, command ...string) error { 14 | return kubernetesutil.Exec(self.REST, self.Config, namespace, podName, containerName, stdin, stdout, nil, false, command...) 15 | } 16 | -------------------------------------------------------------------------------- /turandot/commands/common.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | contextpkg "context" 5 | 6 | "github.com/tliron/commonlog" 7 | "github.com/tliron/go-transcribe" 8 | ) 9 | 10 | const toolName = "turandot" 11 | 12 | var context = contextpkg.TODO() 13 | 14 | var log = commonlog.GetLogger(toolName) 15 | 16 | var filePath string 17 | var directoryPath string 18 | var url string 19 | var component string 20 | var tail int 21 | var follow bool 22 | var all bool 23 | var site string 24 | var wait bool 25 | var registry string 26 | 27 | func Transcriber() *transcribe.Transcriber { 28 | return &transcribe.Transcriber{ 29 | Strict: strict, 30 | Format: format, 31 | ForTerminal: pretty, 32 | Base64: base64, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/1.0/artifacts.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | artifact_types: 4 | 5 | Manifest: 6 | description: >- 7 | Kubernetes resource manifest 8 | file_ext: [ yaml, yml, json ] 9 | 10 | Registry: 11 | description: >- 12 | Artifact stored in a registry 13 | properties: 14 | name: 15 | description: >- 16 | Name of artifact in registry 17 | type: string 18 | registry: 19 | description: >- 20 | Registry name 21 | type: string 22 | default: default 23 | 24 | ContainerImage: 25 | description: >- 26 | OCI container image 27 | derived_from: Registry 28 | file_ext: [ tar, tar.gz, tgz ] 29 | -------------------------------------------------------------------------------- /examples/telephony-network-service/artifacts/asterisk/extensions-vnf.conf.template: -------------------------------------------------------------------------------- 1 | [default] 2 | 3 | exten => 911,1,Log(NOTICE, $__{CALLERID(num)} called $__{EXTEN} (bot)) 4 | same => n,Answer() 5 | same => n,Playback(queue-periodic-announce) 6 | same => n,Hangup() 7 | 8 | exten => _1XX,1,Log(NOTICE, $__{CALLERID(num)} called $__{EXTEN}) 9 | same => n,Dial(PJSIP/trunk/sip:$__{EXTEN}@$TRUNK_IP) 10 | 11 | exten => _X.,1,Log(NOTICE, $__{CALLERID(num)} called $__{EXTEN}) 12 | same => n,Dial(PJSIP/$__{EXTEN}) 13 | same => n,Answer() 14 | same => n,Playback(im-sorry) 15 | same => n,Playback(extension) 16 | same => n,SayDigits($__{EXTEN}) 17 | same => n,Playback(vm-isunavail) 18 | same => n,Playback(cannot-complete-as-dialed) 19 | same => n,Hangup() 20 | -------------------------------------------------------------------------------- /assets/tosca/profiles/orchestration/1.0/js/states/get.js: -------------------------------------------------------------------------------- 1 | 2 | const tosca = require('tosca.lib.utils'); 3 | 4 | let states = {}; 5 | 6 | for (let vertexId in clout.vertexes) { 7 | let vertex = clout.vertexes[vertexId]; 8 | if (!tosca.isNodeTemplate(vertex)) 9 | continue; 10 | let nodeTemplate = vertex.properties; 11 | 12 | if (vertex.metadata.turandot && vertex.metadata.turandot.states) 13 | for (let serviceName in vertex.metadata.turandot.states) { 14 | let nodeState = vertex.metadata.turandot.states[serviceName]; 15 | let serviceStates = states[serviceName]; 16 | if (serviceStates === undefined) 17 | serviceStates = states[serviceName] = {}; 18 | serviceStates[nodeTemplate.name] = nodeState; 19 | } 20 | } 21 | 22 | puccini.write(states); 23 | -------------------------------------------------------------------------------- /examples/helm/scripts/build-csar: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | mkdir --parents "$ROOT/dist" 9 | 10 | function build () { 11 | local ARCHIVE=$1 12 | local WORK=$(mktemp --directory) 13 | 14 | pushd "$HERE/.." > /dev/null 15 | rsync --recursive --relative "${@:2}" "$WORK/" 16 | popd > /dev/null 17 | 18 | pushd "$ROOT/assets/tosca" > /dev/null 19 | rsync --recursive --relative profiles "$WORK/" 20 | popd > /dev/null 21 | 22 | puccini-csar "$ROOT/dist/$ARCHIVE.csar" "$WORK" 23 | 24 | rm --recursive --force "$WORK" 25 | 26 | m "built $ROOT/dist/$ARCHIVE.csar" 27 | } 28 | 29 | build helm \ 30 | helm.yaml \ 31 | artifacts/charts 32 | -------------------------------------------------------------------------------- /assets/profile-generator/kubevirt.yaml: -------------------------------------------------------------------------------- 1 | name: KubeVirt 2 | version: 0.44.1 3 | 4 | open-api: https://raw.githubusercontent.com/kubevirt/kubevirt/v0.44.1/api/openapi-spec/swagger.json 5 | reference-url: http://kubevirt.io/api-reference/v0.44.1/definitions.html 6 | output-dir: ../tosca/profiles/kubevirt/1.0 7 | 8 | groups: 9 | 10 | default: kubevirt.io 11 | imports: 12 | k8s.io.api.core: 13 | namespace_prefix: k8s 14 | file: ../../kubernetes/1.0/profile.yaml 15 | 16 | rename: 17 | 18 | # TOSCA types 19 | object: k8s:Any 20 | number: float 21 | Time: timestamp 22 | MicroTime: timestamp 23 | 24 | override: 25 | 26 | DataVolumeTemplateSpec: 27 | entity: data 28 | 29 | SourceSpec: 30 | fields: 31 | virtualMachine: { type: k8s:Any } # hack 32 | -------------------------------------------------------------------------------- /scripts/_trap: -------------------------------------------------------------------------------- 1 | 2 | function goodbye () { 3 | local DURATION=$(date --date=@$(( "$(date +%s)" - "$TRAP_START_TIME" )) --utc +%T) 4 | local CODE=$1 5 | cd "$TRAP_DIR" 6 | if [ "$CODE" == 0 ]; then 7 | m "$(realpath --relative-to="$ROOT" "$0") succeeded! $DURATION" "$GREEN" 8 | elif [ "$CODE" == abort ]; then 9 | m "Aborted $(realpath --relative-to="$ROOT" "$0")! $DURATION" "$RED" 10 | else 11 | m "Oh no! $(realpath --relative-to="$ROOT" "$0") failed! $DURATION" "$RED" 12 | fi 13 | } 14 | 15 | function trap_EXIT () { 16 | local ERR=$? 17 | goodbye "$ERR" 18 | exit "$ERR" 19 | } 20 | 21 | function trap_INT () { 22 | goodbye abort 23 | trap - EXIT 24 | exit 1 25 | } 26 | 27 | TRAP_DIR=$PWD 28 | TRAP_START_TIME=$(date +%s) 29 | 30 | trap trap_INT INT 31 | 32 | trap trap_EXIT EXIT 33 | -------------------------------------------------------------------------------- /controller/parser/kubernetes-artifact.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/tliron/kutil/util" 5 | "gopkg.in/yaml.v3" 6 | ) 7 | 8 | // 9 | // KubernetesArtifact 10 | // 11 | 12 | type KubernetesArtifact struct { 13 | Name string `yaml:"name"` 14 | Registry string `yaml:"registry"` 15 | SourcePath string `yaml:"sourcePath"` 16 | } 17 | 18 | // 19 | // KubernetesArtifacts 20 | // 21 | 22 | type KubernetesArtifacts []*KubernetesArtifact 23 | 24 | func DecodeKubernetesArtifacts(code string) (KubernetesArtifacts, bool) { 25 | var artifacts struct { 26 | Artifacts KubernetesArtifacts `yaml:"artifacts"` 27 | } 28 | if err := yaml.Unmarshal(util.StringToBytes(code), &artifacts); err == nil { 29 | return artifacts.Artifacts, true 30 | } else { 31 | return nil, false 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/1.0/js/artifacts/get.js: -------------------------------------------------------------------------------- 1 | 2 | const traversal = require('tosca.lib.traversal'); 3 | const tosca = require('tosca.lib.utils'); 4 | 5 | traversal.coerce(); 6 | 7 | let artifacts = []; 8 | 9 | for (let vertexId in clout.vertexes) { 10 | let vertex = clout.vertexes[vertexId]; 11 | if (!tosca.isNodeTemplate(vertex)) 12 | continue; 13 | let nodeTemplate = vertex.properties; 14 | 15 | for (let artifactName in nodeTemplate.artifacts) { 16 | let artifact = nodeTemplate.artifacts[artifactName]; 17 | 18 | if ('cloud.puccini.kubernetes::Registry' in artifact.types) 19 | artifacts.push({ 20 | name: artifact.properties.name, 21 | registry: artifact.properties.registry, 22 | sourcePath: artifact.sourcePath 23 | }); 24 | } 25 | } 26 | 27 | puccini.write({artifacts: artifacts}); 28 | -------------------------------------------------------------------------------- /examples/hello-world/scripts/build-csar: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | mkdir --parents "$ROOT/dist" 9 | 10 | function build () { 11 | local ARCHIVE=$1 12 | local WORK=$(mktemp --directory) 13 | 14 | pushd "$HERE/.." > /dev/null 15 | rsync --recursive --relative "${@:2}" "$WORK/" 16 | popd > /dev/null 17 | 18 | pushd "$ROOT/assets/tosca" > /dev/null 19 | rsync --recursive --relative profiles "$WORK/" 20 | popd > /dev/null 21 | 22 | puccini-csar "$ROOT/dist/$ARCHIVE.csar" "$WORK" 23 | 24 | rm --recursive --force "$WORK" 25 | 26 | m "built $ROOT/dist/$ARCHIVE.csar" 27 | } 28 | 29 | build hello-world \ 30 | hello-world.yaml \ 31 | artifacts/scriptlets \ 32 | artifacts/scripts 33 | -------------------------------------------------------------------------------- /apis/clientset/versioned/typed/turandot.puccini.cloud/v1alpha1/fake/fake_turandot.puccini.cloud_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package fake 4 | 5 | import ( 6 | v1alpha1 "github.com/tliron/turandot/apis/clientset/versioned/typed/turandot.puccini.cloud/v1alpha1" 7 | rest "k8s.io/client-go/rest" 8 | testing "k8s.io/client-go/testing" 9 | ) 10 | 11 | type FakeTurandotV1alpha1 struct { 12 | *testing.Fake 13 | } 14 | 15 | func (c *FakeTurandotV1alpha1) Services(namespace string) v1alpha1.ServiceInterface { 16 | return &FakeServices{c, namespace} 17 | } 18 | 19 | // RESTClient returns a RESTClient that is used to communicate 20 | // with API server by this client implementation. 21 | func (c *FakeTurandotV1alpha1) RESTClient() rest.Interface { 22 | var ret *rest.RESTClient 23 | return ret 24 | } 25 | -------------------------------------------------------------------------------- /turandot/commands/service-clout.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/tliron/kutil/terminal" 6 | "github.com/tliron/kutil/util" 7 | ) 8 | 9 | func init() { 10 | serviceCommand.AddCommand(serviceCloutCommand) 11 | } 12 | 13 | var serviceCloutCommand = &cobra.Command{ 14 | Use: "clout [SERVICE NAME]", 15 | Short: "Get a deployed service's Clout", 16 | Args: cobra.ExactArgs(1), 17 | Run: func(cmd *cobra.Command, args []string) { 18 | serviceName := args[0] 19 | Clout(serviceName) 20 | }, 21 | } 22 | 23 | func Clout(serviceName string) { 24 | // TODO: in cluster mode we must specify the namespace 25 | namespace := "" 26 | 27 | clout, err := NewClient().Turandot().GetServiceClout(namespace, serviceName) 28 | util.FailOnError(err) 29 | 30 | terminal.Println(clout) 31 | } 32 | -------------------------------------------------------------------------------- /examples/self-contained/scripts/build-csar: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | mkdir --parents "$ROOT/dist" 9 | 10 | function build () { 11 | local ARCHIVE=$1 12 | local WORK=$(mktemp --directory) 13 | 14 | pushd "$HERE/.." > /dev/null 15 | rsync --recursive --relative "${@:2}" "$WORK/" 16 | popd > /dev/null 17 | 18 | pushd "$ROOT/assets/tosca" > /dev/null 19 | rsync --recursive --relative profiles "$WORK/" 20 | popd > /dev/null 21 | 22 | puccini-csar "$ROOT/dist/$ARCHIVE.csar" "$WORK" 23 | 24 | rm --recursive --force "$WORK" 25 | 26 | m "built $ROOT/dist/$ARCHIVE.csar" 27 | } 28 | 29 | build self-contained \ 30 | self-contained.yaml \ 31 | artifacts/images \ 32 | artifacts/scriptlets \ 33 | artifacts/scripts 34 | -------------------------------------------------------------------------------- /controller/parser/orchestration-state.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/tliron/kutil/util" 5 | "gopkg.in/yaml.v3" 6 | ) 7 | 8 | // 9 | // OrchestrationNodeState 10 | // 11 | 12 | type OrchestrationNodeState struct { 13 | Mode string `yaml:"mode"` 14 | State string `yaml:"state"` 15 | Message string `yaml:"message"` 16 | } 17 | 18 | // 19 | // OrchestrationNodeStates 20 | // 21 | 22 | type OrchestrationNodeStates map[string]*OrchestrationNodeState 23 | 24 | // 25 | // OrchestrationStates 26 | // 27 | 28 | type OrchestrationStates map[string]OrchestrationNodeStates 29 | 30 | func DecodeOrchestrationStates(code string) (OrchestrationStates, bool) { 31 | var self OrchestrationStates 32 | if err := yaml.Unmarshal(util.StringToBytes(code), &self); err == nil { 33 | return self, true 34 | } else { 35 | return nil, false 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /assets/tosca/profiles/mariadb/nodes.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | imports: 4 | 5 | - capabilities.yaml 6 | - namespace_prefix: k8s 7 | file: ../kubernetes/1.0/profile.yaml 8 | 9 | node_types: 10 | 11 | DB: 12 | capabilities: 13 | metadata: k8s:Metadata 14 | db: DB 15 | 16 | # Operator: 17 | # capabilities: 18 | # metadata: k8s:Metadata 19 | # deployment: k8s:Deployment 20 | # crd: k8s:CustomResourceDefinition 21 | # 22 | # OperatorStorage: 23 | # capabilities: 24 | # metadata: k8s:Metadata 25 | # pv: k8s:PersistentVolume 26 | # pvc: k8s:PersistentVolumeClaim 27 | # 28 | # OperatorServiceAccount: 29 | # capabilities: 30 | # metadata: k8s:Metadata 31 | # service-account: k8s:ServiceAccount 32 | # role: k8s:Role 33 | # role-binding: k8s:RoleBinding 34 | -------------------------------------------------------------------------------- /lab/openshift/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | # oc login -u kubeadmin ... 9 | 10 | oc new-project workspace || true 11 | turandot operator uninstall --wait -v 12 | turandot repository uninstall --wait -v 13 | kubectl delete events --all 14 | 15 | if [ "$1" == -b ]; then 16 | "$ROOT/scripts/build-container-image" 17 | "$ROOT/scripts/publish-container-image" 18 | # Reminder: clean ~/.local/share/containers/ occassionally! 19 | fi 20 | 21 | turandot operator install --site=central --wait -v 22 | turandot repository create default --provider=openshift --wait -v 23 | turandot service deploy hello-world --file=dist/hello-world.csar -v 24 | turandot service deploy helm-hello-world --file=dist/helm-hello-world.csar -v 25 | turandot operator logs --follow 26 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/1.0/profile.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | metadata: 4 | 5 | puccini.scriptlet.import:kubernetes.executions: js/executions.js 6 | puccini.scriptlet.import:kubernetes.artifacts.get: js/artifacts/get.js 7 | puccini.scriptlet.import:kubernetes.artifacts.update: js/artifacts/update.js 8 | puccini.scriptlet.import:kubernetes.resources.get: js/resources/get.js 9 | puccini.scriptlet.import:kubernetes.resources.get-mappings: js/resources/get-mappings.js 10 | puccini.scriptlet.import:kubernetes.resources.update-attributes: js/resources/update-attributes.js 11 | puccini.scriptlet.import:kubernetes.resources.update-mappings: js/resources/update-mappings.js 12 | 13 | namespace: cloud.puccini.kubernetes 14 | 15 | imports: 16 | 17 | - artifacts.yaml 18 | - groups.yaml 19 | - policies.yaml 20 | - interfaces.yaml 21 | - repositories.yaml 22 | -------------------------------------------------------------------------------- /examples/telephony-network-service/artifacts/cloud-config/asterisk-vnf.yaml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | 3 | password: fedora 4 | chpasswd: 5 | expire: false 6 | 7 | users: 8 | - default 9 | - name: admin 10 | groups: wheel 11 | shell: /bin/bash 12 | sudo: ALL=(ALL) NOPASSWD:ALL 13 | ssh-authorized-keys: 14 | - "$KEY" 15 | 16 | #lock_passwd: false 17 | # TODO: doesn't work! 18 | #passwd: $1$VWxq3zKg$fY3uElTYsu16yh7ICDKdT1 19 | # To generate password hash: 20 | # echo mypassword | mkpasswd --stdin --method=SHA-512 --rounds=4096 21 | # or: 22 | # echo mypassword | openssl passwd -stdin -1 23 | 24 | runcmd: 25 | - systemctl enable asterisk.service 26 | - systemctl start asterisk.service 27 | # We don't need cloud-init anymore: 28 | - systemctl disable cloud-config.service 29 | - systemctl disable cloud-final.service 30 | - systemctl disable cloud-init-local.service 31 | - systemctl disable cloud-init.service 32 | -------------------------------------------------------------------------------- /assets/tosca/profiles/orchestration/1.0/js/states/set.js: -------------------------------------------------------------------------------- 1 | 2 | const tosca = require('tosca.lib.utils'); 3 | 4 | let serviceName = puccini.arguments.service; 5 | let nodeTemplateName = puccini.arguments.nodeTemplate; 6 | 7 | let mode = puccini.arguments.mode; 8 | let state = puccini.arguments.state; 9 | let message = puccini.arguments.message; 10 | 11 | for (let vertexId in clout.vertexes) { 12 | let vertex = clout.vertexes[vertexId]; 13 | if (!tosca.isNodeTemplate(vertex)) 14 | continue; 15 | let nodeTemplate = vertex.properties; 16 | if (nodeTemplate.name !== nodeTemplateName) 17 | continue; 18 | 19 | if (!vertex.metadata.turandot) 20 | vertex.metadata.turandot = {}; 21 | if (!vertex.metadata.turandot.states) 22 | vertex.metadata.turandot.states = {} 23 | 24 | vertex.metadata.turandot.states[serviceName] = { 25 | mode: mode, 26 | state: state, 27 | message: message 28 | }; 29 | 30 | puccini.write(clout); 31 | break; 32 | } 33 | -------------------------------------------------------------------------------- /turandot/commands/shell.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/tliron/kutil/kubernetes" 9 | "github.com/tliron/kutil/util" 10 | "github.com/tliron/turandot/controller" 11 | ) 12 | 13 | func Shell(appNameSuffix string, containerName string) { 14 | util.ToRawTerminal(func() error { 15 | return NewClient().Shell(appNameSuffix, containerName, os.Stdin, os.Stdout, os.Stderr) 16 | }) 17 | } 18 | 19 | func (self *Client) Shell(appNameSuffix string, containerName string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { 20 | appName := fmt.Sprintf("%s-%s", controller.NamePrefix, appNameSuffix) 21 | 22 | if podName, err := kubernetes.GetFirstPodName(self.Context, self.Kubernetes, self.Namespace, appName); err == nil { 23 | return kubernetes.Exec(self.REST, self.Config, self.Namespace, podName, containerName, stdin, stdout, stderr, true, "sh") 24 | } else { 25 | return err 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/helm/helm.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | metadata: 4 | 5 | template_name: Helm Example 6 | template_author: Turandot 7 | 8 | imports: 9 | 10 | - namespace_prefix: helm 11 | file: profiles/helm/1.0/profile.yaml 12 | 13 | topology_template: 14 | 15 | inputs: 16 | 17 | namespace: 18 | type: string 19 | default: workspace 20 | 21 | node_templates: 22 | 23 | hello-world: 24 | type: helm:Release 25 | properties: 26 | # If the "name" property is not specified the node template name will be used 27 | chart: { get_artifact: [ SELF, chart ] } 28 | namespace: { get_input: namespace } 29 | values: 30 | nameOverride: helm # is used as both a postfix and a label 31 | service.type: LoadBalancer 32 | service.port: '8080' 33 | artifacts: 34 | chart: 35 | type: helm:Chart 36 | file: artifacts/charts/hello-world.tar.gz 37 | -------------------------------------------------------------------------------- /assets/tosca/profiles/orchestration/1.0/data.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | data_types: 4 | 5 | ProvisioningProfile: 6 | description: >- 7 | All provisioning profiles must have a name unique to the orchestration platform. 8 | 9 | The name can refer to a subtituted service template, which can be included in the CSAR or 10 | in an annotated inventory. 11 | 12 | Variables can be used to further specify variations for the named profile, e.g. the profile 13 | version, optional features, etc. 14 | properties: 15 | name: 16 | type: string 17 | variables: 18 | type: map 19 | entry_schema: string 20 | required: false 21 | 22 | # Execution 23 | 24 | ExecutionRequirements: 25 | description: >- 26 | Base type for execution requirements. 27 | 28 | # Artifacts 29 | 30 | FilePermissions: 31 | derived_from: integer 32 | constraints: 33 | - in_range: [ 0, 0777 ] 34 | -------------------------------------------------------------------------------- /assets/tosca/profiles/orchestration/1.0/js/policies.js: -------------------------------------------------------------------------------- 1 | 2 | const traversal = require('tosca.lib.traversal'); 3 | const tosca = require('tosca.lib.utils'); 4 | 5 | traversal.coerce(); 6 | 7 | let policies = {}; 8 | 9 | for (let vertexId in clout.vertexes) { 10 | let vertex = clout.vertexes[vertexId]; 11 | if (!tosca.isTosca(vertex, 'Policy')) 12 | continue; 13 | let policy = vertex.properties; 14 | 15 | if ('cloud.puccini.turandot.orchestration::Provisioning' in policy.types) 16 | generatePolicy(policy, tosca.getPolicyTargets(vertex), 'provisioning'); 17 | } 18 | 19 | puccini.write(policies); 20 | 21 | function generatePolicy(policy, targets, type) { 22 | for (let t = 0, l = targets.length; t < l; t++) { 23 | let target = targets[t]; 24 | let targetPolicies = policies[target.name]; 25 | if (targetPolicies === undefined) 26 | targetPolicies = policies[target.name] = []; 27 | targetPolicies.push({ 28 | type: type, 29 | properties: policy.properties, 30 | }); 31 | } 32 | } -------------------------------------------------------------------------------- /apis/informers/externalversions/internalinterfaces/factory_interfaces.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package internalinterfaces 4 | 5 | import ( 6 | time "time" 7 | 8 | versioned "github.com/tliron/turandot/apis/clientset/versioned" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | runtime "k8s.io/apimachinery/pkg/runtime" 11 | cache "k8s.io/client-go/tools/cache" 12 | ) 13 | 14 | // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. 15 | type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer 16 | 17 | // SharedInformerFactory a small interface to allow for adding an informer without an import cycle 18 | type SharedInformerFactory interface { 19 | Start(stopCh <-chan struct{}) 20 | InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer 21 | } 22 | 23 | // TweakListOptionsFunc is a function that transforms a v1.ListOptions. 24 | type TweakListOptionsFunc func(*v1.ListOptions) 25 | -------------------------------------------------------------------------------- /scripts/generate-apis: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | . "$HERE/_trap" 7 | 8 | go get -d k8s.io/code-generator@v$K8S_VERSION 9 | CODE_GENERATOR=$(go list -f '{{.Dir}}' -m k8s.io/code-generator@v$K8S_VERSION) 10 | 11 | chmod +x "$CODE_GENERATOR/generate-groups.sh" 12 | 13 | WORK=$(mktemp --directory) 14 | 15 | m "work directory: $WORK" 16 | 17 | copy_function goodbye old_goodbye 18 | function goodbye () { 19 | m "deleting work directory: $WORK" 20 | rm --recursive "$WORK" 21 | old_goodbye $1 22 | } 23 | 24 | GOPATH=$GOPATH \ 25 | "$CODE_GENERATOR/generate-groups.sh" \ 26 | all \ 27 | github.com/tliron/turandot/apis \ 28 | github.com/tliron/turandot/resources \ 29 | turandot.puccini.cloud:v1alpha1 \ 30 | --go-header-file "$HERE/header.go.txt" \ 31 | --output-base "$WORK" 32 | 33 | if [ "$1" == -c ]; then 34 | rm --recursive --force "$ROOT/apis" 35 | fi 36 | 37 | cp --recursive \ 38 | "$WORK/github.com/tliron/turandot/"* \ 39 | "$ROOT/" 40 | -------------------------------------------------------------------------------- /scripts/deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | . "$HERE/_trap" 7 | 8 | for ARG in "$@"; do 9 | case "$ARG" in 10 | -c) 11 | NAMESPACE=$WORKSPACE "$HERE/delete" 12 | ;; 13 | --cluster) 14 | CLUSTER_MODE=true 15 | ;; 16 | esac 17 | done 18 | 19 | export VERSION=1.0 20 | export REGISTRY_URL=$(kubectl_registry_url) 21 | 22 | kubectl create namespace "$WORKSPACE" || true 23 | 24 | m "deploying operator to namespace \"$WORKSPACE\"..." 25 | kubectl_apply_template "$ROOT/assets/kubernetes/custom-resource-definition.yaml" 26 | kubectl_apply_template "$ROOT/assets/namespace.yaml" 27 | if [ "$CLUSTER_MODE" == true ]; then 28 | kubectl_apply_template "$ROOT/assets/kubernetes/cluster-mode-authorization.yaml" 29 | fi 30 | kubectl_apply_template "$ROOT/assets/kubernetes/turandot.yaml" 31 | kubectl_apply_template "$ROOT/assets/kubernetes/hello-world.yaml" 32 | 33 | m "waiting for operator to start..." 34 | kubectl_wait_for_deployment turandot-operator 35 | -------------------------------------------------------------------------------- /examples/telephony-network-service/scripts/build-asterisk-cnf-container-image: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | BASE_IMAGE=fedora:33 9 | IMAGE=asterisk-cnf 10 | LOCAL=localhost/$IMAGE 11 | IMAGES=$(readlink --canonicalize "$HERE/../artifacts/images") 12 | 13 | CONTAINER_ID=$(buildah from "$BASE_IMAGE") 14 | 15 | function r () { 16 | buildah run "$CONTAINER_ID" -- "$@" 17 | } 18 | 19 | m 'installing asterisk packages...' 20 | r dnf --assumeyes install asterisk asterisk-pjsip asterisk-sounds-core-en asterisk-sounds-core-en-ulaw gettext iproute 21 | r dnf --assumeyes clean all 22 | 23 | buildah config \ 24 | --entrypoint '/usr/sbin/asterisk -f -U asterisk -G asterisk -vvvg' \ 25 | --author Turandot \ 26 | --created-by buildah \ 27 | "$CONTAINER_ID" 28 | 29 | buildah commit "$CONTAINER_ID" "$LOCAL" 30 | 31 | mkdir --parents "$IMAGES" 32 | 33 | "$ROOT/scripts/save-portable-container-image" "$LOCAL" "$IMAGES/$IMAGE.tar.gz" 34 | -------------------------------------------------------------------------------- /lab/minikube/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | minikube profile central 9 | kubectl config set-context --current --namespace=workspace 10 | 11 | turandot operator uninstall --wait -v 12 | reposure operator uninstall --wait -v 13 | 14 | kubectl delete events --all 15 | 16 | if [ "$1" == -b ]; then 17 | "$ROOT/scripts/build-container-image" 18 | "$ROOT/scripts/publish-container-image" 19 | # Reminder: clean ~/.local/share/containers/ occassionally! 20 | fi 21 | 22 | reposure operator install --role=view --wait -v 23 | reposure registry create default --provider=minikube --wait -v 24 | reposure image delete default --all -v 25 | 26 | turandot operator install --site=central --role=view --wait -v 27 | turandot service deploy hello-world --file=dist/hello-world.csar -v 28 | turandot service deploy self-contained --file=dist/self-contained.csar -v 29 | turandot service deploy helm --file=dist/helm.csar -v 30 | turandot operator logs --follow 31 | -------------------------------------------------------------------------------- /examples/helm/hello-world/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "hello-world.fullname" . }} 6 | labels: 7 | {{- include "hello-world.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "hello-world.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /assets/tosca/profiles/orchestration/1.0/artifacts.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | imports: 4 | 5 | - data.yaml 6 | 7 | artifact_types: 8 | 9 | Key: 10 | properties: 11 | generate: 12 | type: boolean 13 | default: false 14 | 15 | Deployable: 16 | description: >- 17 | A file intended to be deployed to a container or machine. 18 | 19 | The artifact's "deploy_path" *must* be set. 20 | properties: 21 | permissions: 22 | description: >- 23 | The permissions to apply to the file after deploying it, e.g. via `chmod`. If not 24 | specified then default permissions will be used. 25 | 26 | These permissions will also apply to created directories (in addition to the "executable" 27 | flag). 28 | 29 | Note that literal octals in YAML can be specified beginning with a '0', e.g. 0500. 30 | type: FilePermissions 31 | required: false 32 | 33 | Executable: 34 | derived_from: Deployable 35 | properties: 36 | permissions: 37 | default: 0755 38 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | 2 | builds: 3 | 4 | - id: turandot 5 | main: ./turandot 6 | binary: turandot 7 | goarch: 8 | - amd64 9 | goos: 10 | - linux 11 | - darwin 12 | - windows 13 | ldflags: 14 | - -X 'github.com/tliron/kutil/version.GitVersion={{.Env.VERSION}}' 15 | - -X 'github.com/tliron/kutil/version.GitRevision={{.Env.REVISION}}' 16 | - -X 'github.com/tliron/kutil/version.Timestamp={{.Env.TIMESTAMP}}' 17 | 18 | nfpms: 19 | 20 | - formats: 21 | - rpm 22 | - deb 23 | homepage: https://github.com/tliron/turandot 24 | maintainer: Tal Liron 25 | description: Compose and orchestrate Kubernetes workloads using TOSCA. 26 | license: Apache 2.0 27 | 28 | archives: 29 | 30 | - files: 31 | - README.md 32 | - LICENSE 33 | - NOTICE 34 | - assets/kubernetes/**/* 35 | - assets/tosca/**/* 36 | - examples/**/* 37 | 38 | format_overrides: 39 | - goos: windows 40 | format: zip 41 | 42 | checksum: 43 | 44 | name_template: checksums.txt 45 | 46 | release: 47 | 48 | #disable: true 49 | #prerelease: true 50 | 51 | github: 52 | owner: tliron 53 | name: turandot 54 | -------------------------------------------------------------------------------- /controller/tosca.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | contextpkg "context" 5 | 6 | "github.com/tliron/exturl" 7 | "github.com/tliron/go-ard" 8 | "github.com/tliron/go-transcribe" 9 | "github.com/tliron/kutil/util" 10 | ) 11 | 12 | func (self *Controller) CompileServiceTemplate(context contextpkg.Context, serviceTemplateURL string, inputs map[string]string, cloutPath string, urlContext *exturl.Context) (string, error) { 13 | self.Log.Infof("compiling TOSCA service template: %s", serviceTemplateURL) 14 | self.Log.Infof("inputs: %s", inputs) 15 | 16 | // Decode inputs 17 | inputs_ := make(map[string]ard.Value) 18 | for key, input := range inputs { 19 | var err error 20 | if inputs_[key], _, err = ard.DecodeYAML(util.StringToBytes(input), false); err != nil { 21 | return "", err 22 | } 23 | } 24 | 25 | if file, err := transcribe.OpenFileForWrite(cloutPath); err == nil { 26 | defer file.Close() 27 | if err := CompileTOSCA(context, serviceTemplateURL, inputs_, file, urlContext); err == nil { 28 | return util.GetFileHash(cloutPath) 29 | } else { 30 | return "", err 31 | } 32 | } else { 33 | return "", err 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apis/informers/externalversions/turandot.puccini.cloud/v1alpha1/interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | import ( 6 | internalinterfaces "github.com/tliron/turandot/apis/informers/externalversions/internalinterfaces" 7 | ) 8 | 9 | // Interface provides access to all the informers in this group version. 10 | type Interface interface { 11 | // Services returns a ServiceInformer. 12 | Services() ServiceInformer 13 | } 14 | 15 | type version struct { 16 | factory internalinterfaces.SharedInformerFactory 17 | namespace string 18 | tweakListOptions internalinterfaces.TweakListOptionsFunc 19 | } 20 | 21 | // New returns a new Interface. 22 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 23 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 24 | } 25 | 26 | // Services returns a ServiceInformer. 27 | func (v *version) Services() ServiceInformer { 28 | return &serviceInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 29 | } 30 | -------------------------------------------------------------------------------- /examples/telephony-network-service/profiles/telephony/profile.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | namespace: cloud.puccini.telephony 4 | 5 | imports: 6 | 7 | - namespace_prefix: ns 8 | file: ../network-service/profile.yaml 9 | 10 | node_types: 11 | 12 | PBX: 13 | derived_from: ns:NetworkFunction 14 | properties: 15 | endpoints: 16 | type: map 17 | entry_schema: Endpoint 18 | required: false 19 | capabilities: 20 | trunks: Trunks 21 | requirements: 22 | - trunk: 23 | capability: Trunks 24 | relationship: Trunk 25 | 26 | capability_types: 27 | 28 | Trunks: 29 | derived_from: ns:Connectable 30 | properties: 31 | endpoints: 32 | type: map 33 | entry_schema: Endpoint 34 | required: false 35 | 36 | relationship_types: 37 | 38 | Trunk: 39 | properties: 40 | endpoint: 41 | type: string 42 | attributes: 43 | ip: 44 | type: string 45 | 46 | data_types: 47 | 48 | Endpoint: 49 | properties: 50 | username: 51 | type: string 52 | password: 53 | type: string 54 | -------------------------------------------------------------------------------- /apis/applyconfiguration/internal/internal.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package internal 4 | 5 | import ( 6 | "fmt" 7 | "sync" 8 | 9 | typed "sigs.k8s.io/structured-merge-diff/v4/typed" 10 | ) 11 | 12 | func Parser() *typed.Parser { 13 | parserOnce.Do(func() { 14 | var err error 15 | parser, err = typed.NewParser(schemaYAML) 16 | if err != nil { 17 | panic(fmt.Sprintf("Failed to parse schema: %v", err)) 18 | } 19 | }) 20 | return parser 21 | } 22 | 23 | var parserOnce sync.Once 24 | var parser *typed.Parser 25 | var schemaYAML = typed.YAMLObject(`types: 26 | - name: __untyped_atomic_ 27 | scalar: untyped 28 | list: 29 | elementType: 30 | namedType: __untyped_atomic_ 31 | elementRelationship: atomic 32 | map: 33 | elementType: 34 | namedType: __untyped_atomic_ 35 | elementRelationship: atomic 36 | - name: __untyped_deduced_ 37 | scalar: untyped 38 | list: 39 | elementType: 40 | namedType: __untyped_atomic_ 41 | elementRelationship: atomic 42 | map: 43 | elementType: 44 | namedType: __untyped_deduced_ 45 | elementRelationship: separable 46 | `) 47 | -------------------------------------------------------------------------------- /turandot/commands/operator-install.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/tliron/kutil/util" 6 | ) 7 | 8 | var clusterRole string 9 | var sourceRegistry string 10 | 11 | func init() { 12 | operatorCommand.AddCommand(operatorInstallCommand) 13 | operatorInstallCommand.Flags().StringVarP(&site, "site", "s", "default", "site name") 14 | operatorInstallCommand.Flags().BoolVarP(&clusterMode, "cluster", "c", false, "cluster mode") 15 | operatorInstallCommand.Flags().StringVarP(&clusterRole, "role", "e", "cluster-admin", "cluster role") 16 | operatorInstallCommand.Flags().StringVarP(&sourceRegistry, "registry", "g", "docker.io", "source registry host (use special value \"internal\" to discover internally deployed registry)") 17 | operatorInstallCommand.Flags().BoolVarP(&wait, "wait", "w", false, "wait for installation to succeed") 18 | } 19 | 20 | var operatorInstallCommand = &cobra.Command{ 21 | Use: "install", 22 | Short: "Install the Turandot operator", 23 | Run: func(cmd *cobra.Command, args []string) { 24 | err := NewClient().Turandot().InstallOperator(site, sourceRegistry, wait) 25 | util.FailOnError(err) 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /turandot/commands/template-pull.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/tliron/kutil/util" 8 | ) 9 | 10 | func init() { 11 | templateCommand.AddCommand(templatePullCommand) 12 | templatePullCommand.Flags().StringVarP(®istry, "registry", "r", "default", "name of registry") 13 | } 14 | 15 | var templatePullCommand = &cobra.Command{ 16 | Use: "pull [SERVICE TEMPLATE NAME]", 17 | Short: "Pull a service template as a CSAR from a registry to stdout", 18 | Args: cobra.ExactArgs(1), 19 | Run: func(cmd *cobra.Command, args []string) { 20 | serviceTemplateName := args[0] 21 | PullServiceTemplate(serviceTemplateName) 22 | }, 23 | } 24 | 25 | func PullServiceTemplate(serviceTemplateName string) { 26 | turandot := NewClient().Turandot() 27 | registry_, err := turandot.Reposure.RegistryClient().Get(namespace, registry) 28 | util.FailOnError(err) 29 | command, err := turandot.Reposure.SurrogateCommandClient(registry_) 30 | util.FailOnError(err) 31 | 32 | imageName := turandot.RegistryImageNameForServiceTemplateName(serviceTemplateName) 33 | err = command.PullLayer(imageName, os.Stdout) 34 | util.FailOnError(err) 35 | } 36 | -------------------------------------------------------------------------------- /examples/telephony-network-service/artifacts/scripts/asterisk/configure-cnf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | cd /tmp 5 | 6 | if [ -f configured ]; then 7 | exit 0 8 | fi 9 | 10 | function ip_address () { 11 | # See: https://stackoverflow.com/a/26694162 12 | ip -family inet address show $1 | grep --only-matching --perl-regexp '(?<=inet\s)\d+(\.\d+){3}' 13 | } 14 | 15 | EXTERNAL_IP=$1 16 | 17 | if [ "$EXTERNAL_IP" == '' ]; then 18 | exit 1 19 | fi 20 | 21 | CONTROL_PLANE_IP=$(ip_address eth0) 22 | DATA_PLANE_IP=$(ip_address net1) 23 | TRUNK_IP=192.68.1.1 # CNF 24 | 25 | cp /dev/stdin clout.yaml 26 | 27 | cat extensions.conf.template | __=\\$ TRUNK_IP=$TRUNK_IP envsubst > extensions.conf 28 | cat pjsip.conf.template | CONTROL_PLANE_IP=$CONTROL_PLANE_IP DATA_PLANE_IP=$DATA_PLANE_IP EXTERNAL_IP=$EXTERNAL_IP TRUNK_IP=$TRUNK_IP envsubst > pjsip.conf 29 | 30 | mv pjsip.conf /etc/asterisk/pjsip.conf 31 | mv rtp.conf /etc/asterisk/rtp.conf 32 | mv extensions.conf /etc/asterisk/extensions.conf 33 | 34 | chown asterisk:asterisk \ 35 | /etc/asterisk/pjsip.conf \ 36 | /etc/asterisk/rtp.conf \ 37 | /etc/asterisk/extensions.conf 38 | 39 | sudo asterisk -rx 'core restart now' 40 | 41 | touch configured 42 | -------------------------------------------------------------------------------- /scripts/build-container-image: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | . "$HERE/_trap" 7 | 8 | "$HERE/build" 9 | 10 | # https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/building_running_and_managing_containers/using_red_hat_universal_base_images_standard_minimal_and_runtimes 11 | 12 | function build () { 13 | local EXECUTABLE=$1 14 | local IMAGE=$EXECUTABLE 15 | local LOCAL=localhost/$IMAGE 16 | local BASE_IMAGE=registry.access.redhat.com/ubi8/ubi 17 | # registry.redhat.io/ubi8/ubi 18 | # note: ubi-minimal does not have "tar" which is needed for kubectl cp 19 | 20 | local CONTAINER_ID=$(buildah from "$BASE_IMAGE") 21 | #buildah run "$CONTAINER_ID" -- dnf --assumeyes install ansible 22 | #buildah run "$CONTAINER_ID" -- dnf --assumeyes clean all 23 | buildah copy "$CONTAINER_ID" "$GOPATH/bin/$EXECUTABLE" /usr/bin/ 24 | buildah copy "$CONTAINER_ID" "$(which helm)" /usr/bin/ 25 | buildah config \ 26 | --entrypoint "/usr/bin/$EXECUTABLE" \ 27 | --author Turandot \ 28 | --created-by buildah \ 29 | "$CONTAINER_ID" 30 | buildah commit "$CONTAINER_ID" "$LOCAL" 31 | } 32 | 33 | build turandot-operator 34 | -------------------------------------------------------------------------------- /examples/helm/hello-world/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: hello-world 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | appVersion: 1.16.0 24 | -------------------------------------------------------------------------------- /turandot/commands/service-output.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/tliron/kutil/terminal" 6 | "github.com/tliron/kutil/util" 7 | ) 8 | 9 | func init() { 10 | serviceCommand.AddCommand(serviceOutputCommand) 11 | } 12 | 13 | var serviceOutputCommand = &cobra.Command{ 14 | Use: "output [SERVICE NAME] [OUTPUT NAME]", 15 | Short: "Get a deployed service's output", 16 | Args: cobra.ExactArgs(2), 17 | Run: func(cmd *cobra.Command, args []string) { 18 | serviceName := args[0] 19 | outputName := args[1] 20 | ServiceOutput(serviceName, outputName) 21 | }, 22 | } 23 | 24 | func ServiceOutput(serviceName string, outputName string) { 25 | // TODO: in cluster mode we must specify the namespace 26 | namespace := "" 27 | 28 | service, err := NewClient().Turandot().GetService(namespace, serviceName) 29 | util.FailOnError(err) 30 | 31 | if service.Status.Outputs != nil { 32 | if output, ok := service.Status.Outputs[outputName]; ok { 33 | // TODO: unpack the YAML 34 | // TODO: support output in various formats 35 | terminal.Println(output) 36 | return 37 | } 38 | } 39 | 40 | util.Failf("output %q not found in service %q", outputName, serviceName) 41 | } 42 | -------------------------------------------------------------------------------- /assets/tosca/profiles/mariadb/capabilities.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | imports: 4 | 5 | - namespace_prefix: k8s 6 | file: ../kubernetes/1.0/profile.yaml 7 | 8 | capability_types: 9 | 10 | Connectable: {} 11 | 12 | # See: https://github.com/abalki001/mariadb-operator/tree/master/deploy 13 | DB: 14 | metadata: 15 | turandot.apiVersion: mariadb.persistentsys/v1alpha1 16 | turandot.kind: MariaDB 17 | derived_from: Connectable 18 | properties: 19 | database: 20 | description: >- 21 | New database name. 22 | type: string 23 | username: 24 | type: string 25 | password: 26 | type: string 27 | rootpwd: 28 | description: >- 29 | Root user password. 30 | type: string 31 | image: 32 | description: >- 33 | Image URL. 34 | type: string 35 | default: mariadb/server:10.3 36 | size: 37 | description: >- 38 | Size of the deployment. 39 | type: integer 40 | default: 1 41 | attributes: 42 | nodes: 43 | description: >- 44 | The names of the pods. 45 | type: list 46 | entry_schema: string 47 | -------------------------------------------------------------------------------- /apis/informers/externalversions/turandot.puccini.cloud/interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package turandot 4 | 5 | import ( 6 | internalinterfaces "github.com/tliron/turandot/apis/informers/externalversions/internalinterfaces" 7 | v1alpha1 "github.com/tliron/turandot/apis/informers/externalversions/turandot.puccini.cloud/v1alpha1" 8 | ) 9 | 10 | // Interface provides access to each of this group's versions. 11 | type Interface interface { 12 | // V1alpha1 provides access to shared informers for resources in V1alpha1. 13 | V1alpha1() v1alpha1.Interface 14 | } 15 | 16 | type group struct { 17 | factory internalinterfaces.SharedInformerFactory 18 | namespace string 19 | tweakListOptions internalinterfaces.TweakListOptionsFunc 20 | } 21 | 22 | // New returns a new Interface. 23 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 24 | return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 25 | } 26 | 27 | // V1alpha1 returns a new v1alpha1.Interface. 28 | func (g *group) V1alpha1() v1alpha1.Interface { 29 | return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) 30 | } 31 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/_env" 6 | 7 | "$HERE/build" 8 | 9 | #curl --silent --fail --location https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | \ 10 | #sh -s -- -b "$GOPATH/bin" 11 | go install github.com/goreleaser/goreleaser@latest 12 | 13 | # Uses the current tag for the release version 14 | # (So you likely want to tag before running this) 15 | 16 | WORK=$(mktemp --directory) 17 | 18 | function the_end () { 19 | local ERR=$? 20 | rm --recursive --force "$WORK" 21 | exit $ERR 22 | } 23 | 24 | trap the_end EXIT 25 | 26 | # Create a clean copy of our repo 27 | rsync --recursive "$ROOT/" "$WORK" 28 | cd "$WORK" 29 | git clean -xdf 30 | find . -type f -name .gitignore -exec rm {} \; 31 | 32 | git_version 33 | 34 | ARGS= 35 | if [ "$1" == -t ]; then 36 | ARGS='--snapshot --skip-publish' 37 | fi 38 | 39 | VERSION=$VERSION REVISION=$REVISION TIMESTAMP=$TIMESTAMP \ 40 | "$GOPATH/bin/goreleaser" \ 41 | --parallelism=$(nproc) \ 42 | --skip-validate $ARGS 43 | 44 | # Copy releases back here 45 | rm --recursive --force "$ROOT/dist/release" 46 | mkdir --parents "$ROOT/dist" 47 | rsync --recursive "$WORK/dist/" "$ROOT/dist/release" 48 | -------------------------------------------------------------------------------- /turandot/commands/service-mode.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/tliron/kutil/terminal" 6 | "github.com/tliron/kutil/util" 7 | ) 8 | 9 | func init() { 10 | serviceCommand.AddCommand(serviceModeCommand) 11 | } 12 | 13 | var serviceModeCommand = &cobra.Command{ 14 | Use: "mode [SERVICE NAME] [[MODE]]", 15 | Short: "Get or set a deployed service's mode", 16 | Args: cobra.RangeArgs(1, 2), 17 | Run: func(cmd *cobra.Command, args []string) { 18 | if len(args) == 2 { 19 | SetMode(args[0], args[1]) 20 | } else { 21 | GetMode(args[0]) 22 | } 23 | }, 24 | } 25 | 26 | func GetMode(serviceName string) { 27 | // TODO: in cluster mode we must specify the namespace 28 | namespace := "" 29 | 30 | service, err := NewClient().Turandot().GetService(namespace, serviceName) 31 | util.FailOnError(err) 32 | terminal.Println(service.Status.Mode) 33 | } 34 | 35 | func SetMode(serviceName string, mode string) { 36 | // TODO: in cluster mode we must specify the namespace 37 | namespace := "" 38 | 39 | turandot := NewClient().Turandot() 40 | service, err := turandot.GetService(namespace, serviceName) 41 | util.FailOnError(err) 42 | _, err = turandot.UpdateServiceMode(service, mode) 43 | util.FailOnError(err) 44 | } 45 | -------------------------------------------------------------------------------- /examples/helm/hello-world/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "hello-world.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 5 | apiVersion: networking.k8s.io/v1beta1 6 | {{- else -}} 7 | apiVersion: extensions/v1beta1 8 | {{- end }} 9 | kind: Ingress 10 | metadata: 11 | name: {{ $fullName }} 12 | labels: 13 | {{- include "hello-world.labels" . | nindent 4 }} 14 | {{- with .Values.ingress.annotations }} 15 | annotations: 16 | {{- toYaml . | nindent 4 }} 17 | {{- end }} 18 | spec: 19 | {{- if .Values.ingress.tls }} 20 | tls: 21 | {{- range .Values.ingress.tls }} 22 | - hosts: 23 | {{- range .hosts }} 24 | - {{ . | quote }} 25 | {{- end }} 26 | secretName: {{ .secretName }} 27 | {{- end }} 28 | {{- end }} 29 | rules: 30 | {{- range .Values.ingress.hosts }} 31 | - host: {{ .host | quote }} 32 | http: 33 | paths: 34 | {{- range .paths }} 35 | - path: {{ . }} 36 | backend: 37 | serviceName: {{ $fullName }} 38 | servicePort: {{ $svcPort }} 39 | {{- end }} 40 | {{- end }} 41 | {{- end }} 42 | -------------------------------------------------------------------------------- /turandot/commands/delegate-delete.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/tliron/kutil/util" 6 | ) 7 | 8 | func init() { 9 | delegateCommand.AddCommand(delegateDeleteCommand) 10 | delegateDeleteCommand.Flags().BoolVarP(&all, "all", "a", false, "delete all delegates") 11 | } 12 | 13 | var delegateDeleteCommand = &cobra.Command{ 14 | Use: "delete [[DELEGATE NAME]]", 15 | Short: "Delete a delegate (or all delegates)", 16 | Args: cobra.RangeArgs(0, 1), 17 | Run: func(cmd *cobra.Command, args []string) { 18 | if len(args) == 1 { 19 | delegateName := args[0] 20 | DeleteDelegate(delegateName) 21 | } else if all { 22 | DeleteAllDelegates() 23 | } else { 24 | util.Fail("must provide delegate name or specify \"--all\"") 25 | } 26 | }, 27 | } 28 | 29 | func DeleteDelegate(delegateName string) { 30 | err := NewClient().Turandot().DeleteDelegate(delegateName) 31 | util.FailOnError(err) 32 | } 33 | 34 | func DeleteAllDelegates() { 35 | turandot := NewClient().Turandot() 36 | delegates, err := turandot.ListDelegates() 37 | util.FailOnError(err) 38 | for _, delegate := range delegates { 39 | log.Infof("deleting delegate: %s", delegate) 40 | err := turandot.DeleteDelegate(delegate) 41 | util.FailOnError(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /controller/parser/clout-attribute-value.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/tliron/go-transcribe" 5 | ) 6 | 7 | // 8 | // CloutAttributeValue 9 | // 10 | 11 | type CloutAttributeValue struct { 12 | CapabilityName string `json:"capability"` 13 | AttributeName string `json:"attribute"` 14 | Value any `json:"value"` 15 | } 16 | 17 | // 18 | // CloutAttributeValueList 19 | // 20 | 21 | type CloutAttributeValueList []*CloutAttributeValue 22 | 23 | // 24 | // CloutAttributeValues 25 | // 26 | 27 | type CloutAttributeValues map[string]CloutAttributeValueList 28 | 29 | func NewCloutAttributeValues() CloutAttributeValues { 30 | return make(CloutAttributeValues) 31 | } 32 | 33 | func (self CloutAttributeValues) Set(vertexId string, capabilityName string, attributeName string, value any) { 34 | self[vertexId] = append(self[vertexId], &CloutAttributeValue{ 35 | CapabilityName: capabilityName, 36 | AttributeName: attributeName, 37 | Value: value, 38 | }) 39 | } 40 | 41 | func (self CloutAttributeValues) JSON() map[string]string { 42 | map_ := make(map[string]string) 43 | for vertexId, list := range self { 44 | if value, err := transcribe.NewTranscriber().StringifyJSON(list); err == nil { 45 | map_[vertexId] = value 46 | } 47 | } 48 | return map_ 49 | } 50 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubevirt/1.0/js/kubernetes-resources-pre-get.js: -------------------------------------------------------------------------------- 1 | 2 | const tosca = require('tosca.lib.utils'); 3 | 4 | exports.plugin = function() { 5 | for (let vertexId in clout.vertexes) { 6 | let vertex = clout.vertexes[vertexId]; 7 | if (!tosca.isNodeTemplate(vertex)) 8 | continue; 9 | let nodeTemplate = vertex.properties; 10 | 11 | for (let artifactName in nodeTemplate.artifacts) { 12 | let artifact = nodeTemplate.artifacts[artifactName]; 13 | 14 | if ('cloud.puccini.kubevirt::CloudConfig' in artifact.types) { 15 | let cloudConfig = puccini.loadString(artifact.sourcePath); 16 | 17 | let variables = artifact.properties.variables; 18 | if (variables !== undefined) { 19 | variables = clout.newCoercible(variables, vertex); 20 | variables = variables.coerce(); 21 | if (variables) 22 | for (let name in variables) { 23 | let r = new RegExp(escapeRegExp(name), 'g'); 24 | cloudConfig = cloudConfig.replace(r, variables[name]); 25 | } 26 | } 27 | 28 | if (artifact.properties.base64) 29 | cloudConfig = puccini.btoa(cloudConfig); 30 | 31 | artifact.$artifact = cloudConfig; 32 | } 33 | } 34 | } 35 | } 36 | 37 | function escapeRegExp(s) { 38 | // See: https://stackoverflow.com/a/3561711 39 | return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 40 | } 41 | -------------------------------------------------------------------------------- /controller/policy.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | contextpkg "context" 5 | 6 | "github.com/tliron/exturl" 7 | "github.com/tliron/turandot/controller/parser" 8 | resources "github.com/tliron/turandot/resources/turandot.puccini.cloud/v1alpha1" 9 | ) 10 | 11 | func (self *Controller) processPolicies(context contextpkg.Context, policies parser.OrchestrationPolicies, service *resources.Service, urlContext *exturl.Context) error { 12 | for nodeTemplateName, nodePolicies := range policies { 13 | self.Log.Infof("processing policies for node template %s", nodeTemplateName) 14 | for _, policy := range nodePolicies { 15 | switch policy_ := policy.(type) { 16 | case *parser.OrchestrationProvisioningPolicy: 17 | self.Log.Infof("instantiable: %t", policy_.Instantiable) 18 | self.Log.Infof("substitutable: %t", policy_.Substitutable) 19 | self.Log.Infof("sites: %s", policy_.Sites) 20 | 21 | // TODO: should mode be defined in policy? 22 | mode := "normal" 23 | 24 | // Substitutions 25 | if policy_.Substitutable { 26 | for _, site := range policy_.Sites { 27 | if err := self.Substitute(context, service.Namespace, nodeTemplateName, policy_.SubstitutionInputs, mode, site, urlContext); err != nil { 28 | return err 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /turandot/commands/delegate-list.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/tliron/go-ard" 8 | "github.com/tliron/kutil/terminal" 9 | "github.com/tliron/kutil/util" 10 | ) 11 | 12 | func init() { 13 | delegateCommand.AddCommand(delegateListCommand) 14 | } 15 | 16 | var delegateListCommand = &cobra.Command{ 17 | Use: "list", 18 | Short: "List delegates", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | ListDelegates() 21 | }, 22 | } 23 | 24 | func ListDelegates() { 25 | delegates, err := NewClient().Turandot().ListDelegates() 26 | util.FailOnError(err) 27 | if len(delegates) == 0 { 28 | return 29 | } 30 | sort.Strings(delegates) 31 | 32 | switch format { 33 | case "": 34 | // TODO fill table 35 | table := terminal.NewTable(maxWidth, "Name", "Server", "Namespace") 36 | for _, delegate := range delegates { 37 | table.Add(delegate, "TODO", "TODO") 38 | } 39 | table.Print() 40 | 41 | case "bare": 42 | for _, delegate := range delegates { 43 | terminal.Println(delegate) 44 | } 45 | 46 | default: 47 | list := make(ard.List, len(delegates)) 48 | for index, delegate := range delegates { 49 | map_ := make(ard.StringMap) 50 | map_["Name"] = delegate 51 | map_["Server"] = "" 52 | map_["Namespace"] = "" 53 | list[index] = map_ 54 | } 55 | Transcriber().Write(list) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /client/tls.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "crypto/x509" 5 | "fmt" 6 | 7 | "github.com/tliron/kutil/util" 8 | core "k8s.io/api/core/v1" 9 | meta "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | const tlsMountPath = "/tls" 13 | 14 | var tlsCertificatePath = fmt.Sprintf("%s/%s", tlsMountPath, core.TLSCertKey) 15 | var tlsKeyPath = fmt.Sprintf("%s/%s", tlsMountPath, core.TLSPrivateKeyKey) 16 | 17 | func (self *Client) GetSecret(namespace string, secretName string) (*core.Secret, error) { 18 | return self.Kubernetes.CoreV1().Secrets(namespace).Get(self.Context, secretName, meta.GetOptions{}) 19 | } 20 | 21 | func (self *Client) GetSecretTLSCertPool(namespace string, secretName string, secretDataKey string) (*x509.CertPool, error) { 22 | if secret, err := self.GetSecret(namespace, secretName); err == nil { 23 | if secretDataKey == "" { 24 | secretDataKey = core.TLSCertKey 25 | } 26 | 27 | switch secret.Type { 28 | case core.SecretTypeTLS, core.SecretTypeServiceAccountToken: 29 | if bytes, ok := secret.Data[secretDataKey]; ok { 30 | return util.ParseX509CertificatePool(bytes) 31 | } else { 32 | return nil, fmt.Errorf("no data key %q in %q secret: %s", secretDataKey, secret.Type, secret.Data) 33 | } 34 | 35 | default: 36 | return nil, fmt.Errorf("unsupported TLS secret type: %s", secret.Type) 37 | } 38 | } else { 39 | return nil, err 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/telephony-network-service/scripts/build-asterisk-vnf-container-image: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # For virt-customize: 5 | # dnf install libguestfs-tools-c 6 | 7 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 8 | . "$HERE/../../../scripts/_env" 9 | . "$ROOT/scripts/_trap" 10 | 11 | # https://alt.fedoraproject.org/cloud/ 12 | 13 | BASE_IMAGE=scratch 14 | DISK_NAME=Fedora-Cloud-Base-33-1.2.x86_64.qcow2 15 | URL=https://download.fedoraproject.org/pub/fedora/linux/releases/33/Cloud/x86_64/images/$DISK_NAME 16 | DISK=$TMPDIR/$DISK_NAME 17 | IMAGE=asterisk-vnf 18 | LOCAL=localhost/$IMAGE 19 | IMAGES=$(readlink --canonicalize "$HERE/../artifacts/images") 20 | 21 | m 'downloading base disk...' 22 | rm --force "$DISK" 23 | wget --quiet --show-progress --output-document="$DISK" "$URL" 24 | 25 | m 'installing asterisk packages...' 26 | virt-customize \ 27 | --add "$DISK" \ 28 | --selinux-relabel \ 29 | --install asterisk,asterisk-pjsip,asterisk-sounds-core-en,asterisk-sounds-core-en-ulaw,tcpdump,grc 30 | 31 | CONTAINER_ID=$(buildah from "$BASE_IMAGE") 32 | 33 | buildah copy "$CONTAINER_ID" "$DISK" "/disk/$DISK_NAME" 34 | 35 | buildah config \ 36 | --author Turandot \ 37 | --created-by buildah \ 38 | "$CONTAINER_ID" 39 | 40 | buildah commit "$CONTAINER_ID" "$LOCAL" 41 | 42 | mkdir --parents "$IMAGES" 43 | 44 | "$ROOT/scripts/save-portable-container-image" "$LOCAL" "$IMAGES/$IMAGE.tar.gz" 45 | -------------------------------------------------------------------------------- /turandot/commands/logs.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/tliron/kutil/kubernetes" 9 | "github.com/tliron/kutil/util" 10 | "github.com/tliron/turandot/controller" 11 | ) 12 | 13 | func Logs(appNameSuffix string, containerName string) { 14 | // TODO: what happens if we follow more than one log? 15 | readers, err := NewClient().Logs(appNameSuffix, containerName, tail, follow) 16 | util.FailOnError(err) 17 | for _, reader := range readers { 18 | defer reader.Close() 19 | } 20 | for _, reader := range readers { 21 | io.Copy(os.Stdout, reader) 22 | } 23 | } 24 | 25 | func (self *Client) Logs(appNameSuffix string, containerName string, tail int, follow bool) ([]io.ReadCloser, error) { 26 | appName := fmt.Sprintf("%s-%s", controller.NamePrefix, appNameSuffix) 27 | 28 | if podNames, err := kubernetes.GetPodNames(self.Context, self.Kubernetes, self.Namespace, appName); err == nil { 29 | readers := make([]io.ReadCloser, len(podNames)) 30 | for index, podName := range podNames { 31 | if reader, err := kubernetes.Log(self.Context, self.Kubernetes, self.Namespace, podName, containerName, tail, follow); err == nil { 32 | readers[index] = reader 33 | } else { 34 | for i := 0; i < index; i++ { 35 | readers[i].Close() 36 | } 37 | return nil, err 38 | } 39 | } 40 | return readers, nil 41 | } else { 42 | return nil, err 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/telephony-network-service/artifacts/scripts/asterisk/configure-vnf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd /tmp 5 | 6 | if [ -f configured ]; then 7 | exit 0 8 | fi 9 | 10 | function ip_address () { 11 | # See: https://stackoverflow.com/a/26694162 12 | ip -family inet address show $1 | grep --only-matching --perl-regexp '(?<=inet\s)\d+(\.\d+){3}' 13 | } 14 | 15 | EXTERNAL_IP=$1 16 | 17 | if [ "$EXTERNAL_IP" == '' ]; then 18 | exit 1 19 | fi 20 | 21 | CONTROL_PLANE_IP=$(ip_address eth0) 22 | DATA_PLANE_IP=$(ip_address eth1) 23 | TRUNK_IP=192.68.1.1 # CNF 24 | 25 | cp /dev/stdin clout.yaml 26 | 27 | cat extensions.conf.template | __=\\$ TRUNK_IP=$TRUNK_IP envsubst > extensions.conf 28 | cat pjsip.conf.template | CONTROL_PLANE_IP=$CONTROL_PLANE_IP DATA_PLANE_IP=$DATA_PLANE_IP EXTERNAL_IP=$EXTERNAL_IP TRUNK_IP=$TRUNK_IP envsubst > pjsip.conf 29 | 30 | sudo mv pjsip.conf /etc/asterisk/pjsip.conf 31 | sudo mv rtp.conf /etc/asterisk/rtp.conf 32 | sudo mv extensions.conf /etc/asterisk/extensions.conf 33 | 34 | sudo chown asterisk:asterisk \ 35 | /etc/asterisk/pjsip.conf \ 36 | /etc/asterisk/rtp.conf \ 37 | /etc/asterisk/extensions.conf 38 | 39 | sudo chcon system_u:object_r:asterisk_etc_t:s0 \ 40 | /etc/asterisk/pjsip.conf \ 41 | /etc/asterisk/rtp.conf \ 42 | /etc/asterisk/extensions.conf 43 | 44 | sudo asterisk -rx 'core restart now' 45 | #sudo systemctl restart asterisk.service 46 | 47 | touch configured 48 | -------------------------------------------------------------------------------- /turandot/commands/service-delete.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/tliron/kutil/util" 6 | ) 7 | 8 | func init() { 9 | serviceCommand.AddCommand(serviceDeleteCommand) 10 | serviceDeleteCommand.Flags().BoolVarP(&all, "all", "a", false, "delete all services") 11 | } 12 | 13 | var serviceDeleteCommand = &cobra.Command{ 14 | Use: "delete [[SERVICE NAME]]", 15 | Short: "Delete a deployed service (or all services)", 16 | Args: cobra.RangeArgs(0, 1), 17 | Run: func(cmd *cobra.Command, args []string) { 18 | if len(args) == 1 { 19 | serviceName := args[0] 20 | DeleteService(serviceName) 21 | } else if all { 22 | DeleteAllServices() 23 | } else { 24 | util.Fail("must provide service name or specify \"--all\"") 25 | } 26 | }, 27 | } 28 | 29 | func DeleteService(serviceName string) { 30 | // TODO: in cluster mode we must specify the namespace 31 | namespace := "" 32 | 33 | err := NewClient().Turandot().DeleteService(namespace, serviceName) 34 | util.FailOnError(err) 35 | } 36 | 37 | func DeleteAllServices() { 38 | turandot := NewClient().Turandot() 39 | services, err := turandot.ListServices() 40 | util.FailOnError(err) 41 | for _, service := range services.Items { 42 | log.Infof("deleting service: %s/%s", service.Namespace, service.Name) 43 | err := turandot.DeleteService(service.Namespace, service.Name) 44 | util.FailOnError(err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /assets/tosca/profiles/helm/1.0/nodes.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | node_types: 4 | 5 | Release: 6 | description: >- 7 | Helm release. 8 | properties: 9 | name: 10 | description: >- 11 | Release name. If not specified will use node template name. 12 | type: string 13 | required: false 14 | chart: 15 | description: >- 16 | URL to the chart (as with "helm install"). Can point to an archive (tarball) or path to 17 | a local directory. This can be a "get_artifact" function call (see the Chart artifact 18 | type). 19 | type: string 20 | version: 21 | type: string 22 | required: false 23 | namespace: 24 | type: string 25 | required: false 26 | hooks: 27 | description: >- 28 | Set to true to install hooks. 29 | type: boolean 30 | default: false 31 | values: 32 | description: >- 33 | Set chart values (as with "helm install --set-string"). If both "values" and "valuesUrl" 34 | are used and overlap, "values" will override "valuesUrl". 35 | type: map 36 | entry_schema: string 37 | required: false 38 | valuesUrl: 39 | description: >- 40 | URL to a YAML file with chart values (as with "helm install --values"). 41 | type: string 42 | required: false 43 | -------------------------------------------------------------------------------- /examples/telephony-network-service/artifacts/asterisk/pjsip-cnf.conf.template: -------------------------------------------------------------------------------- 1 | [local] 2 | type=transport 3 | allow_reload=true 4 | protocol=udp 5 | bind=$CONTROL_PLANE_IP 6 | external_media_address=$EXTERNAL_IP 7 | 8 | [external] 9 | type=transport 10 | allow_reload=true 11 | protocol=udp 12 | bind=$DATA_PLANE_IP 13 | 14 | ; templates 15 | 16 | [endpoint](!) 17 | type=endpoint 18 | context=default 19 | transport=local 20 | disallow=all 21 | allow=ulaw 22 | 23 | [auth](!) 24 | type=auth 25 | auth_type=userpass 26 | password=password 27 | 28 | [aor](!) 29 | type=aor 30 | max_contacts=5 31 | remove_existing=true 32 | 33 | ; anonymous 34 | ; (handled by es_pjsip_endpoint_identifier_anonymous.so) 35 | 36 | [anonymous](endpoint) 37 | 38 | ; 100 39 | 40 | [100](endpoint) 41 | auth=100 42 | aors=100 43 | 44 | [100](auth) 45 | username=100 46 | 47 | [100](aor) 48 | 49 | ; 101 50 | 51 | [101](endpoint) 52 | auth=101 53 | aors=101 54 | 55 | [101](auth) 56 | username=101 57 | 58 | [101](aor) 59 | 60 | ; trunk 61 | 62 | [trunk] 63 | type=registration 64 | transport=external 65 | outbound_auth=trunk 66 | server_uri=sip:trunk@$TRUNK_IP 67 | client_uri=sip:trunk@$TRUNK_IP 68 | 69 | [trunk] 70 | type=identify 71 | endpoint=trunk 72 | match=$TRUNK_IP 73 | 74 | [trunk](endpoint) 75 | transport=external 76 | media_address=$DATA_PLANE_IP 77 | outbound_auth=trunk 78 | aors=trunk 79 | 80 | [trunk](auth) 81 | username=trunk 82 | 83 | [trunk](aor) 84 | contact=sip:$TRUNK_IP 85 | -------------------------------------------------------------------------------- /examples/telephony-network-service/artifacts/asterisk/pjsip-vnf.conf.template: -------------------------------------------------------------------------------- 1 | [local] 2 | type=transport 3 | allow_reload=true 4 | protocol=udp 5 | bind=$CONTROL_PLANE_IP 6 | external_media_address=$EXTERNAL_IP 7 | 8 | [external] 9 | type=transport 10 | allow_reload=true 11 | protocol=udp 12 | bind=$DATA_PLANE_IP 13 | 14 | ; templates 15 | 16 | [endpoint](!) 17 | type=endpoint 18 | context=default 19 | transport=local 20 | disallow=all 21 | allow=ulaw 22 | 23 | [auth](!) 24 | type=auth 25 | auth_type=userpass 26 | password=password 27 | 28 | [aor](!) 29 | type=aor 30 | max_contacts=5 31 | remove_existing=true 32 | 33 | ; anonymous 34 | ; (handled by es_pjsip_endpoint_identifier_anonymous.so) 35 | 36 | [anonymous](endpoint) 37 | 38 | ; 200 39 | 40 | [200](endpoint) 41 | auth=200 42 | aors=200 43 | 44 | [200](auth) 45 | username=200 46 | 47 | [200](aor) 48 | 49 | ; 201 50 | 51 | [201](endpoint) 52 | auth=201 53 | aors=201 54 | 55 | [201](auth) 56 | username=201 57 | 58 | [201](aor) 59 | 60 | ; trunk 61 | 62 | [trunk] 63 | type=registration 64 | transport=external 65 | outbound_auth=trunk 66 | server_uri=sip:trunk@$TRUNK_IP 67 | client_uri=sip:trunk@$TRUNK_IP 68 | 69 | [trunk] 70 | type=identify 71 | endpoint=trunk 72 | match=$TRUNK_IP 73 | 74 | [trunk](endpoint) 75 | transport=external 76 | media_address=$DATA_PLANE_IP 77 | outbound_auth=trunk 78 | aors=trunk 79 | 80 | [trunk](auth) 81 | username=trunk 82 | 83 | [trunk](aor) 84 | contact=sip:$TRUNK_IP 85 | -------------------------------------------------------------------------------- /examples/telephony-network-service/scripts/build-csars: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | mkdir --parents "$ROOT/dist" 9 | 10 | function build () { 11 | local ARCHIVE=$1 12 | local WORK=$(mktemp --directory) 13 | 14 | pushd "$HERE/.." > /dev/null 15 | rsync --recursive --relative "${@:2}" "$WORK/" 16 | popd > /dev/null 17 | 18 | pushd "$ROOT/assets/tosca" > /dev/null 19 | rsync --recursive --relative profiles "$WORK/" 20 | popd > /dev/null 21 | 22 | puccini-csar "$ROOT/dist/$ARCHIVE.csar" "$WORK" 23 | 24 | rm --recursive --force "$WORK" 25 | 26 | m "built $ROOT/dist/$ARCHIVE.csar" 27 | } 28 | 29 | mkdir --parents "$HERE/../artifacts/binaries/" 30 | cp "$(which puccini-clout)" "$HERE/../artifacts/binaries/" 31 | 32 | build telephony-network-service \ 33 | telephony-network-service.yaml \ 34 | profiles 35 | 36 | build asterisk-cnf \ 37 | asterisk-cnf.yaml \ 38 | profiles \ 39 | artifacts/scripts \ 40 | artifacts/binaries \ 41 | artifacts/asterisk \ 42 | artifacts/images/asterisk-cnf.tar.gz 43 | 44 | build asterisk-vnf \ 45 | asterisk-vnf.yaml \ 46 | profiles \ 47 | artifacts/scripts \ 48 | artifacts/binaries \ 49 | artifacts/asterisk \ 50 | artifacts/keypairs \ 51 | artifacts/cloud-config \ 52 | artifacts/images/asterisk-vnf.tar.gz 53 | 54 | build simple-data-plane \ 55 | simple-data-plane.yaml \ 56 | profiles 57 | -------------------------------------------------------------------------------- /resources/turandot.puccini.cloud/v1alpha1/scheme.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | meta "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | 8 | group "github.com/tliron/turandot/resources/turandot.puccini.cloud" 9 | ) 10 | 11 | // Group version used to register these objects 12 | // Note: Generator *requires* it to be called "SchemeGroupVersion" 13 | var SchemeGroupVersion = schema.GroupVersion{Group: group.GroupName, Version: Version} 14 | 15 | // Takes an unqualified kind and returns a group-qualified GroupKind 16 | // Note: Generator *requires* it to be called "Kind" 17 | func Kind(kind string) schema.GroupKind { 18 | return SchemeGroupVersion.WithKind(kind).GroupKind() 19 | } 20 | 21 | // Takes an unqualified resource and returns a group-qualified GroupResource 22 | // Note: Generator *requires* it to be called "Resource" 23 | func Resource(resource string) schema.GroupResource { 24 | return SchemeGroupVersion.WithResource(resource).GroupResource() 25 | } 26 | 27 | // Registers this API group and version to a scheme 28 | // Note: Generator *requires* it to be called "AddToScheme" 29 | var AddToScheme = schemeBuilder.AddToScheme 30 | 31 | var schemeBuilder = runtime.NewSchemeBuilder(func(scheme *runtime.Scheme) error { 32 | scheme.AddKnownTypes( 33 | SchemeGroupVersion, 34 | &Service{}, 35 | &ServiceList{}, 36 | ) 37 | meta.AddToGroupVersion(scheme, SchemeGroupVersion) 38 | return nil 39 | }) 40 | -------------------------------------------------------------------------------- /apis/clientset/versioned/fake/register.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package fake 4 | 5 | import ( 6 | turandotv1alpha1 "github.com/tliron/turandot/resources/turandot.puccini.cloud/v1alpha1" 7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | schema "k8s.io/apimachinery/pkg/runtime/schema" 10 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 11 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 12 | ) 13 | 14 | var scheme = runtime.NewScheme() 15 | var codecs = serializer.NewCodecFactory(scheme) 16 | 17 | var localSchemeBuilder = runtime.SchemeBuilder{ 18 | turandotv1alpha1.AddToScheme, 19 | } 20 | 21 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 22 | // of clientsets, like in: 23 | // 24 | // import ( 25 | // "k8s.io/client-go/kubernetes" 26 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 27 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 28 | // ) 29 | // 30 | // kclientset, _ := kubernetes.NewForConfig(c) 31 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 32 | // 33 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 34 | // correctly. 35 | var AddToScheme = localSchemeBuilder.AddToScheme 36 | 37 | func init() { 38 | v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) 39 | utilruntime.Must(AddToScheme(scheme)) 40 | } 41 | -------------------------------------------------------------------------------- /turandot/commands/delegate-set.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os/user" 5 | "path/filepath" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/tliron/kutil/util" 9 | ) 10 | 11 | var delegateKubeconfigPath string 12 | var delegateContext string 13 | var delegateNamespace string 14 | 15 | func init() { 16 | var defaultKubeconfigPath string 17 | if u, err := user.Current(); err == nil { 18 | defaultKubeconfigPath = filepath.Join(u.HomeDir, ".kube", "config") 19 | } 20 | 21 | delegateCommand.AddCommand(delegateSetCommand) 22 | delegateSetCommand.Flags().StringVar(&delegateKubeconfigPath, "delegate-kubeconfig", defaultKubeconfigPath, "path to delegate delegate Kubernetes configuration") 23 | delegateSetCommand.Flags().StringVar(&delegateContext, "delegate-context", "", "override current context in delegate Kubernetes configuration") 24 | delegateSetCommand.Flags().StringVar(&delegateNamespace, "delegate-namespace", "", "namespace (overrides context namespace in delegate Kubernetes configuration)") 25 | } 26 | 27 | var delegateSetCommand = &cobra.Command{ 28 | Use: "set [DELEGATE NAME]", 29 | Short: "Update or create a delegate", 30 | Args: cobra.ExactArgs(1), 31 | Run: func(cmd *cobra.Command, args []string) { 32 | delegateName := args[0] 33 | SetDelegate(delegateName) 34 | }, 35 | } 36 | 37 | func SetDelegate(delegateName string) { 38 | err := NewClient().Turandot().SetDelegate(delegateName, delegateKubeconfigPath, delegateContext, delegateNamespace) 39 | util.FailOnError(err) 40 | } 41 | -------------------------------------------------------------------------------- /examples/telephony-network-service/profiles/network-service/profile.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | namespace: cloud.puccini.network-service 4 | 5 | node_types: 6 | 7 | NetworkPlane: 8 | properties: 9 | annotations: 10 | type: map 11 | entry_schema: string 12 | required: false 13 | capabilities: 14 | connection: Connectable 15 | 16 | NetworkFunction: 17 | properties: 18 | annotations: 19 | type: map 20 | entry_schema: string 21 | required: false 22 | capabilities: 23 | deployment: Deployable 24 | requirements: 25 | - connection: 26 | capability: Connectable 27 | relationship: Connection 28 | 29 | Router: 30 | derived_from: NetworkFunction 31 | capabilities: 32 | route: Routable 33 | 34 | capability_types: 35 | 36 | Connectable: {} 37 | 38 | Routable: 39 | derived_from: Connectable 40 | 41 | Deployable: 42 | attributes: 43 | ingress: 44 | type: Route 45 | 46 | relationship_types: 47 | 48 | Connection: {} 49 | 50 | Routing: 51 | derived_from: Connection 52 | properties: 53 | routes: 54 | type: list 55 | entry_schema: Route 56 | 57 | data_types: 58 | 59 | NetworkSubnet: 60 | derived_from: string 61 | 62 | NetworkAddress: 63 | derived_from: string 64 | 65 | Route: 66 | properties: 67 | destination: 68 | type: NetworkSubnet 69 | gateway: 70 | type: NetworkAddress 71 | -------------------------------------------------------------------------------- /apis/clientset/versioned/scheme/register.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package scheme 4 | 5 | import ( 6 | turandotv1alpha1 "github.com/tliron/turandot/resources/turandot.puccini.cloud/v1alpha1" 7 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | schema "k8s.io/apimachinery/pkg/runtime/schema" 10 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 11 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 12 | ) 13 | 14 | var Scheme = runtime.NewScheme() 15 | var Codecs = serializer.NewCodecFactory(Scheme) 16 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 17 | var localSchemeBuilder = runtime.SchemeBuilder{ 18 | turandotv1alpha1.AddToScheme, 19 | } 20 | 21 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 22 | // of clientsets, like in: 23 | // 24 | // import ( 25 | // "k8s.io/client-go/kubernetes" 26 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 27 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 28 | // ) 29 | // 30 | // kclientset, _ := kubernetes.NewForConfig(c) 31 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 32 | // 33 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 34 | // correctly. 35 | var AddToScheme = localSchemeBuilder.AddToScheme 36 | 37 | func init() { 38 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 39 | utilruntime.Must(AddToScheme(Scheme)) 40 | } 41 | -------------------------------------------------------------------------------- /assets/tosca/profiles/orchestration/1.0/interfaces.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | imports: 4 | 5 | - data.yaml 6 | 7 | interface_types: 8 | 9 | # Execution 10 | 11 | Execution: 12 | description: >- 13 | Base type for executions. 14 | inputs: 15 | mode: 16 | description: >- 17 | If not specified then the interface name will be used. If the interface names has "." 18 | in it then the last "." and what follows it will be stripped. E.g. interface 19 | "normal.1" will have mode "normal". 20 | 21 | This makes it easy to specify several executions for the same mode. Note that 22 | executions will occur in alphanumeric sorting order of the interface names. 23 | type: string 24 | required: false 25 | requirements: 26 | type: ExecutionRequirements 27 | required: false 28 | 29 | Scriptlet: 30 | description: >- 31 | Executes a scriptlet within the Clout. 32 | derived_from: Execution 33 | inputs: 34 | scriptlet: 35 | description: >- 36 | Scriptlet name. 37 | type: string 38 | arguments: 39 | description: >- 40 | Scriptlet arguments. 41 | 42 | Implicit arguments: 43 | 44 | * "service" is the name of the service. 45 | * "nodeTemplate" is the name of the node template. 46 | 47 | TODO: in the case of interfaces on relationships? 48 | type: map 49 | entry_schema: string 50 | required: false 51 | -------------------------------------------------------------------------------- /controller/events.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | 6 | resources "github.com/tliron/turandot/resources/turandot.puccini.cloud/v1alpha1" 7 | core "k8s.io/api/core/v1" 8 | ) 9 | 10 | const ( 11 | EventCompiled = "Compiled" 12 | EventCompilationError = "CompilationError" 13 | EventInstantiated = "Instantiated" 14 | EventInstantiationError = "InstantiationError" 15 | EventCloutUpdateError = "CloutUpdateError" 16 | ) 17 | 18 | func (self *Controller) EventCompiled(service *resources.Service) { 19 | self.Events.Event(service, core.EventTypeNormal, EventCompiled, "Service template compiled successfully") 20 | } 21 | 22 | func (self *Controller) EventCompilationError(service *resources.Service, err error) { 23 | self.Events.Event(service, core.EventTypeWarning, EventCompilationError, fmt.Sprintf("Service template compilation error: %s", err.Error())) 24 | } 25 | 26 | func (self *Controller) EventInstantiated(service *resources.Service) { 27 | self.Events.Event(service, core.EventTypeNormal, EventInstantiated, "Service instantiated successfully") 28 | } 29 | 30 | func (self *Controller) EventInstantiationError(service *resources.Service, err error) { 31 | self.Events.Event(service, core.EventTypeWarning, EventInstantiationError, fmt.Sprintf("Service instantiation error: %s", err.Error())) 32 | } 33 | 34 | func (self *Controller) EventCloutUpdateError(service *resources.Service, err error) { 35 | self.Events.Event(service, core.EventTypeWarning, EventCloutUpdateError, fmt.Sprintf("Service Clout update error: %s", err.Error())) 36 | } 37 | -------------------------------------------------------------------------------- /client/registry.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | reposure "github.com/tliron/reposure/resources/reposure.puccini.cloud/v1alpha1" 8 | ) 9 | 10 | const serviceTemplateArtifactCategory = "service-templates" 11 | 12 | func (self *Client) GetRegistryURLForCSAR(registry *reposure.Registry, artifactName string) (string, error) { 13 | if address, err := self.Reposure.RegistryClient().GetHost(registry); err == nil { 14 | return fmt.Sprintf("docker://%s/%s?format=csar", address, artifactName), nil 15 | } else { 16 | return "", err 17 | } 18 | } 19 | 20 | func (self *Client) GetRegistryServiceTemplateURL(registry *reposure.Registry, serviceTemplateName string) (string, error) { 21 | return self.GetRegistryURLForCSAR(registry, self.RegistryImageNameForServiceTemplateName(serviceTemplateName)) 22 | } 23 | 24 | // Utils 25 | 26 | func (self *Client) RegistryImageNameForServiceTemplateName(serviceTemplateName string) string { 27 | // Note: OpenShift registry permissions require the namespace as the tag category 28 | return fmt.Sprintf("%s/%s-%s", self.Namespace, serviceTemplateArtifactCategory, serviceTemplateName) 29 | } 30 | 31 | func (self *Client) ServiceTemplateNameForRegistryImageName(artifactName string) (string, bool) { 32 | prefix := fmt.Sprintf("%s/%s-", self.Namespace, serviceTemplateArtifactCategory) 33 | if strings.HasPrefix(artifactName, prefix) { 34 | name := artifactName[len(prefix):] 35 | if colon := strings.Index(name, ":"); colon != -1 { 36 | name = name[:colon] 37 | } 38 | return name, true 39 | } else { 40 | return "", false 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/README.md: -------------------------------------------------------------------------------- 1 | Kubernetes Profile 2 | ================== 3 | 4 | Node Types 5 | ---------- 6 | 7 | Kubernetes Metadata 8 | ------------------- 9 | 10 | Capability Type Metadata 11 | ------------------------ 12 | 13 | `turandot.apiVersion`: Kubernetes resource API version. This metadata value triggers a Kubernetes 14 | resource generation. 15 | 16 | `turandot.kind`: Kubernetes resource kind. If not specified will default to the capability type name. 17 | 18 | `turandot.move[.#]`: Move a value or branch in the resource. The format is "->". 19 | Adding "." to the key is optional and used for ensuring the order of move operations, as the keys 20 | are sorted alphabetically before processing. 21 | 22 | `turandot.copy[.#]`: Copy a value or branch in the resource. The format is "->". 23 | Adding "." to the key is optional and used for ensuring the order of move operations, as the keys 24 | are sorted alphabetically before processing. Note that moves are processed before copies. 25 | 26 | Data Type Metadata 27 | ------------------ 28 | 29 | Note that TOSCA scalar-unit types will always be converted to numbers (floats or integers as 30 | appropriate). 31 | 32 | Property Metadata 33 | ----------------- 34 | 35 | `puccini.data-type:turandot.ignore`: When "true" (a string) will *not* export the property to the 36 | Kubernetes resource. 37 | 38 | Attribute Metadata 39 | ------------------ 40 | 41 | `puccini.data-type:turandot.mapping`: The value is the path within the Kubernetes resource from which to 42 | map the attribute value. Note that both "status." and "spec." can be used. 43 | -------------------------------------------------------------------------------- /examples/helm/README.md: -------------------------------------------------------------------------------- 1 | Helm Example 2 | ============ 3 | 4 | This example demonstrates a Helm chart included as an artifact within a CSAR. 5 | 6 | To deploy this example you can generally follow the instructions for the 7 | [Hello World example](../../TUTORIAL.md), though make sure to build the chart 8 | artifact first. E.g.: 9 | 10 | examples/scripts/build-chart 11 | examples/scripts/build-csar 12 | turandot service deploy helm --file=dist/helm.csar 13 | 14 | As long as you have LoadBalancer ingress support on your cluster (such as 15 | Minikube's "tunnel"), then you can then use curl or a web browser to access the 16 | deployed service: 17 | 18 | IP=$(kubectl get services --selector=app.kubernetes.io/name=helm --output=jsonpath={.items[0].status.loadBalancer.ingress[0].ip}) 19 | xdg-open http://$IP:8080 20 | 21 | 22 | Helper Scripts 23 | -------------- 24 | 25 | * [Package the Helm chart](scripts/build-chart) 26 | * [Package as CSAR file](scripts/build-csar) 27 | 28 | 29 | How the Helm Chart Was Created 30 | ------------------------------ 31 | 32 | We used `helm create hello-world`. Unfortunately, this example chart assumes privileged containers on 33 | the host that are not allowed out-of-the-box on OpenShift. To ensure it would work on OpenShift we 34 | made two changes: 35 | 36 | * In `value.yaml` we changed `image.repository` to `bitnami/nginx`, which points to an NGINX container 37 | image that does not require a priveleged container 38 | ([documentation](https://hub.docker.com/r/bitnami/nginx)). 39 | * In `templates/deployment.yaml` we changed the port from 80 to 8080 (again, because port 80 requires 40 | a privileged container). 41 | -------------------------------------------------------------------------------- /apis/informers/externalversions/generic.go: -------------------------------------------------------------------------------- 1 | // Code generated by informer-gen. DO NOT EDIT. 2 | 3 | package externalversions 4 | 5 | import ( 6 | "fmt" 7 | 8 | v1alpha1 "github.com/tliron/turandot/resources/turandot.puccini.cloud/v1alpha1" 9 | schema "k8s.io/apimachinery/pkg/runtime/schema" 10 | cache "k8s.io/client-go/tools/cache" 11 | ) 12 | 13 | // GenericInformer is type of SharedIndexInformer which will locate and delegate to other 14 | // sharedInformers based on type 15 | type GenericInformer interface { 16 | Informer() cache.SharedIndexInformer 17 | Lister() cache.GenericLister 18 | } 19 | 20 | type genericInformer struct { 21 | informer cache.SharedIndexInformer 22 | resource schema.GroupResource 23 | } 24 | 25 | // Informer returns the SharedIndexInformer. 26 | func (f *genericInformer) Informer() cache.SharedIndexInformer { 27 | return f.informer 28 | } 29 | 30 | // Lister returns the GenericLister. 31 | func (f *genericInformer) Lister() cache.GenericLister { 32 | return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) 33 | } 34 | 35 | // ForResource gives generic access to a shared informer of the matching type 36 | // TODO extend this to unknown resources with a client pool 37 | func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { 38 | switch resource { 39 | // Group=turandot.puccini.cloud, Version=v1alpha1 40 | case v1alpha1.SchemeGroupVersion.WithResource("services"): 41 | return &genericInformer{resource: resource.GroupResource(), informer: f.Turandot().V1alpha1().Services().Informer()}, nil 42 | 43 | } 44 | 45 | return nil, fmt.Errorf("no informer found for %v", resource) 46 | } 47 | -------------------------------------------------------------------------------- /apis/applyconfiguration/turandot.puccini.cloud/v1alpha1/servicetemplate.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | // ServiceTemplateApplyConfiguration represents an declarative configuration of the ServiceTemplate type for use 6 | // with apply. 7 | type ServiceTemplateApplyConfiguration struct { 8 | Direct *ServiceTemplateDirectApplyConfiguration `json:"direct,omitempty"` 9 | Indirect *ServiceTemplateIndirectApplyConfiguration `json:"indirect,omitempty"` 10 | } 11 | 12 | // ServiceTemplateApplyConfiguration constructs an declarative configuration of the ServiceTemplate type for use with 13 | // apply. 14 | func ServiceTemplate() *ServiceTemplateApplyConfiguration { 15 | return &ServiceTemplateApplyConfiguration{} 16 | } 17 | 18 | // WithDirect sets the Direct field in the declarative configuration to the given value 19 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 20 | // If called multiple times, the Direct field is set to the value of the last call. 21 | func (b *ServiceTemplateApplyConfiguration) WithDirect(value *ServiceTemplateDirectApplyConfiguration) *ServiceTemplateApplyConfiguration { 22 | b.Direct = value 23 | return b 24 | } 25 | 26 | // WithIndirect sets the Indirect field in the declarative configuration to the given value 27 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 28 | // If called multiple times, the Indirect field is set to the value of the last call. 29 | func (b *ServiceTemplateApplyConfiguration) WithIndirect(value *ServiceTemplateIndirectApplyConfiguration) *ServiceTemplateApplyConfiguration { 30 | b.Indirect = value 31 | return b 32 | } 33 | -------------------------------------------------------------------------------- /turandot-profile-generator/configuration.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // 4 | // Configuration 5 | // 6 | 7 | type Configuration struct { 8 | Name string `yaml:"name"` 9 | Version string `yaml:"version"` 10 | OpenAPI string `yaml:"open-api"` 11 | ReferenceURL string `yaml:"reference-url"` 12 | OutputDir string `yaml:"output-dir"` 13 | Groups Groups `yaml:"groups"` 14 | Exclude []string `yaml:"exclude"` 15 | Rename map[string]string `yaml:"rename"` 16 | Add map[string]*Type `yaml:"add"` 17 | Override map[string]*Type `yaml:"override"` 18 | } 19 | 20 | // 21 | // Groups 22 | // 23 | 24 | type Groups struct { 25 | Default string `yaml:"default"` 26 | Imports map[string]Import `yaml:"imports"` 27 | } 28 | 29 | // 30 | // Import 31 | // 32 | 33 | type Import struct { 34 | NamespacePrefix string `yaml:"namespace_prefix"` 35 | File string `yaml:"file"` 36 | } 37 | 38 | // 39 | // Type 40 | // 41 | 42 | type Type struct { 43 | Entity string `yaml:"entity"` 44 | Metadata []map[string]string `yaml:"metadata"` 45 | Description string `yaml:"description"` 46 | Fields map[string]*Field `yaml:"fields"` 47 | DerivedFrom string `yaml:"derived_from"` 48 | Derive map[string][]string `yaml:"derive"` 49 | } 50 | 51 | // 52 | // Field 53 | // 54 | 55 | type Field struct { 56 | Type string `yaml:"type"` 57 | EntrySchema string `yaml:"entry_schema"` 58 | Description string `yaml:"description"` 59 | Default string `yaml:"default"` 60 | Required *bool `yaml:"required"` 61 | Constraints []string `yaml:"constraints"` 62 | } 63 | -------------------------------------------------------------------------------- /apis/applyconfiguration/utils.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package applyconfiguration 4 | 5 | import ( 6 | turandotpuccinicloudv1alpha1 "github.com/tliron/turandot/apis/applyconfiguration/turandot.puccini.cloud/v1alpha1" 7 | v1alpha1 "github.com/tliron/turandot/resources/turandot.puccini.cloud/v1alpha1" 8 | schema "k8s.io/apimachinery/pkg/runtime/schema" 9 | ) 10 | 11 | // ForKind returns an apply configuration type for the given GroupVersionKind, or nil if no 12 | // apply configuration type exists for the given GroupVersionKind. 13 | func ForKind(kind schema.GroupVersionKind) interface{} { 14 | switch kind { 15 | // Group=turandot.puccini.cloud, Version=v1alpha1 16 | case v1alpha1.SchemeGroupVersion.WithKind("Service"): 17 | return &turandotpuccinicloudv1alpha1.ServiceApplyConfiguration{} 18 | case v1alpha1.SchemeGroupVersion.WithKind("ServiceNodeModeState"): 19 | return &turandotpuccinicloudv1alpha1.ServiceNodeModeStateApplyConfiguration{} 20 | case v1alpha1.SchemeGroupVersion.WithKind("ServiceSpec"): 21 | return &turandotpuccinicloudv1alpha1.ServiceSpecApplyConfiguration{} 22 | case v1alpha1.SchemeGroupVersion.WithKind("ServiceStatus"): 23 | return &turandotpuccinicloudv1alpha1.ServiceStatusApplyConfiguration{} 24 | case v1alpha1.SchemeGroupVersion.WithKind("ServiceTemplate"): 25 | return &turandotpuccinicloudv1alpha1.ServiceTemplateApplyConfiguration{} 26 | case v1alpha1.SchemeGroupVersion.WithKind("ServiceTemplateDirect"): 27 | return &turandotpuccinicloudv1alpha1.ServiceTemplateDirectApplyConfiguration{} 28 | case v1alpha1.SchemeGroupVersion.WithKind("ServiceTemplateIndirect"): 29 | return &turandotpuccinicloudv1alpha1.ServiceTemplateIndirectApplyConfiguration{} 30 | 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /lab/minikube/test-multi: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../../scripts/_env" 6 | . "$ROOT/scripts/_trap" 7 | 8 | minikube profile edge 9 | kubectl config set-context edge --namespace=workspace 10 | turandot operator uninstall --wait -v 11 | turandot repository uninstall --wait -v 12 | kubectl delete events --all 13 | 14 | minikube profile central 15 | kubectl config set-context central --namespace=workspace 16 | turandot operator uninstall --wait -v 17 | turandot repository uninstall --wait -v 18 | kubectl delete events --all 19 | 20 | if [ "$1" == -b ]; then 21 | "$ROOT/scripts/build-container-image" 22 | "$ROOT/scripts/publish-container-image" 23 | # Reminder: clean ~/.local/share/containers/ occassionally! 24 | fi 25 | 26 | turandot operator install --site=central --wait -v 27 | turandot repository install --wait -v 28 | turandot repository create default --provider=turandot --wait -v 29 | #turandot repository create default --provider=minikube --wait -v 30 | 31 | turandot service deploy hello-world --file=dist/hello-world.csar -v 32 | turandot service deploy helm-hello-world --file=dist/helm-hello-world.csar -v 33 | 34 | turandot delegate set edge --delegate-context=edge -v 35 | 36 | turandot template register telephony-network-service --file=dist/telephony-network-service.csar -v 37 | turandot template register simple-data-plane --file=dist/simple-data-plane.csar -v 38 | turandot template register asterisk-cnf --file=dist/asterisk-cnf.csar -v 39 | turandot template register asterisk-vnf --file=dist/asterisk-vnf.csar -v 40 | 41 | turandot service deploy telephony-network-service --template=telephony-network-service --input=namespace=workspace -v 42 | 43 | turandot operator logs --follow 44 | -------------------------------------------------------------------------------- /lab/connect-kube-dns: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 5 | . "$HERE/../scripts/_env" 6 | 7 | # Do we really need a new service? 8 | # Unfortunately, yes: port-forward will only work with TCP, but we need UDP for proper DNS support 9 | #kubectl port-forward service/kube-dns --namespace=kube-system 5353:53 10 | 11 | m 'exposing kube-dns...' 12 | cat <<- EOT | kubectl apply -f - 13 | kind: Service 14 | apiVersion: v1 15 | 16 | metadata: 17 | name: kube-dns-external 18 | namespace: kube-system 19 | labels: 20 | k8s-app: kube-dns 21 | 22 | spec: 23 | type: LoadBalancer 24 | selector: 25 | k8s-app: kube-dns 26 | ports: 27 | - name: dns 28 | protocol: UDP 29 | port: 53 30 | targetPort: 53 31 | EOT 32 | 33 | m 'waiting for kube-dns-external IP...' 34 | while [ -z "$IP" ]; do 35 | IP=$(kubectl get services kube-dns-external --namespace=kube-system --output=jsonpath={.status.loadBalancer.ingress[0].ip}) 36 | sleep 1 37 | done 38 | 39 | m $IP 40 | 41 | m 'enabling dnsmasq plugin for NetworkManager...' 42 | cat <<- EOT | sudo tee /etc/NetworkManager/conf.d/00-dnsmasq.conf > /dev/null 43 | [main] 44 | dns=dnsmasq 45 | EOT 46 | 47 | # http://www.thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html 48 | cat <<- EOT | sudo tee /etc/NetworkManager/dnsmasq.d/minikube.conf > /dev/null 49 | server=/cluster.local/$IP 50 | addn-hosts=/etc/hosts 51 | EOT 52 | 53 | sudo systemctl reload NetworkManager.service 54 | 55 | # If we were trying to use localhost#5353 (in the port-forward situation) we would also need to 56 | # change SELinux permissions: 57 | #semanage permissive --add dnsmasq_t 58 | 59 | # test 60 | #dig @127.0.0.1 a asterisk-vnf-tcp.workspace.svc.cluster.local 61 | #host asterisk-vnf-tcp.workspace.svc.cluster.local 62 | -------------------------------------------------------------------------------- /turandot-profile-generator/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "unicode" 7 | ) 8 | 9 | func isVersionNewer(newer string, older string) bool { 10 | if newer_ := NewVersion(newer); newer_ != nil { 11 | if older_ := NewVersion(older); older_ != nil { 12 | return newer_.IsNewer(older_) 13 | } 14 | } 15 | return false 16 | } 17 | 18 | // 19 | // Version 20 | // 21 | 22 | type Version struct { 23 | Major int 24 | Minor int 25 | Patch int 26 | } 27 | 28 | // e.g. v1alpha2 29 | func NewVersion(text string) *Version { 30 | if strings.HasPrefix(text, "v") { 31 | phase := 1 32 | var major, modifier, patch string 33 | for _, rune_ := range text[1:] { 34 | if phase == 1 { 35 | if unicode.IsDigit(rune_) { 36 | major += string(rune_) 37 | } else { 38 | phase = 2 39 | } 40 | } 41 | 42 | if phase == 2 { 43 | if unicode.IsDigit(rune_) { 44 | phase = 3 45 | } else { 46 | modifier += string(rune_) 47 | } 48 | } 49 | 50 | if phase == 3 { 51 | patch += string(rune_) 52 | } 53 | } 54 | 55 | major_, _ := strconv.Atoi(major) 56 | patch_, _ := strconv.Atoi(patch) 57 | 58 | var minor_ int 59 | switch modifier { 60 | case "alpha": 61 | minor_ = -2 62 | case "beta": 63 | minor_ = -1 64 | } 65 | 66 | return &Version{ 67 | Major: major_, 68 | Minor: minor_, 69 | Patch: patch_, 70 | } 71 | } else { 72 | return nil 73 | } 74 | } 75 | 76 | func (self *Version) IsNewer(older *Version) bool { 77 | if self.Major > older.Major { 78 | return true 79 | } else if self.Major == older.Major { 80 | if self.Minor > older.Minor { 81 | return true 82 | } else if self.Minor == older.Minor { 83 | return self.Patch > older.Patch 84 | } else { 85 | return false 86 | } 87 | } else { 88 | return false 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /examples/helm/hello-world/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "hello-world.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "hello-world.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "hello-world.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "hello-world.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /controller/delegation.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | kubernetesutil "github.com/tliron/kutil/kubernetes" 8 | reposurepkg "github.com/tliron/reposure/apis/clientset/versioned" 9 | turandotpkg "github.com/tliron/turandot/apis/clientset/versioned" 10 | clientpkg "github.com/tliron/turandot/client" 11 | apiextensionspkg "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 12 | kubernetespkg "k8s.io/client-go/kubernetes" 13 | ) 14 | 15 | func (self *Controller) NewDelegate(name string) (*clientpkg.Client, error) { 16 | configPath := filepath.Join(self.CachePath, "delegates", fmt.Sprintf("%s.yaml", name)) 17 | if config, err := kubernetesutil.NewConfig(configPath, ""); err == nil { 18 | namespace, _ := kubernetesutil.GetConfiguredNamespace(configPath, "") 19 | 20 | var kubernetes *kubernetespkg.Clientset 21 | kubernetes, err := kubernetespkg.NewForConfig(config) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | var apiExtensions *apiextensionspkg.Clientset 27 | apiExtensions, err = apiextensionspkg.NewForConfig(config) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | var turandot *turandotpkg.Clientset 33 | turandot, err = turandotpkg.NewForConfig(config) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | var reposure *reposurepkg.Clientset 39 | reposure, err = reposurepkg.NewForConfig(config) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | rest := kubernetes.CoreV1().RESTClient() 45 | 46 | return clientpkg.NewClient( 47 | kubernetes, 48 | apiExtensions, 49 | turandot, 50 | reposure, 51 | rest, 52 | config, 53 | self.Context, 54 | false, 55 | "", 56 | namespace, 57 | NamePrefix, 58 | PartOf, 59 | ManagedBy, 60 | OperatorImageName, 61 | CacheDirectory, 62 | fmt.Sprintf("turandot.client.%s", name), 63 | ), nil 64 | } else { 65 | return nil, err 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /assets/tosca/profiles/kubernetes/1.0/interfaces.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | imports: 4 | 5 | - data.yaml 6 | - namespace_prefix: o11n 7 | file: ../../orchestration/1.0/profile.yaml 8 | 9 | interface_types: 10 | 11 | Command: 12 | derived_from: o11n:Execution 13 | description: >- 14 | Base type for Kubernetes command executions. 15 | 16 | The implementation is the command to execute. 17 | inputs: 18 | command: 19 | description: >- 20 | Command plus its arguments. 21 | type: list 22 | entry_schema: string 23 | constraints: 24 | - min_length: 1 25 | artifacts: 26 | description: >- 27 | A list of artifact names to copy to the target before execution. 28 | 29 | The artifact's "deploy_path" *must* be set. Directories will be created if they do not 30 | exist (using `mkdir --parents`). 31 | 32 | Note that there is special support for the "permissions" property of Deployable type 33 | artifacts. 34 | type: list 35 | entry_schema: string 36 | required: false 37 | 38 | ContainerCommand: 39 | description: >- 40 | Uses the SPDY executor on the "exec" subresource of the pods. 41 | derived_from: Command 42 | inputs: 43 | selector: 44 | type: LabelSelector 45 | required: false 46 | pods: 47 | type: ExecutionPods 48 | default: all 49 | container: 50 | description: >- 51 | The name of the container in the pods in which to execute. 52 | 53 | This is only required if the pods have more than one container. 54 | type: string 55 | required: false 56 | 57 | SSHCommand: 58 | description: >- 59 | Uses SSH to execute commands. 60 | derived_from: Command 61 | inputs: 62 | host: 63 | type: string 64 | username: 65 | type: string 66 | key: 67 | type: string 68 | -------------------------------------------------------------------------------- /examples/helm/hello-world/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "hello-world.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "hello-world.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "hello-world.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "hello-world.labels" -}} 37 | helm.sh/chart: {{ include "hello-world.chart" . }} 38 | {{ include "hello-world.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "hello-world.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "hello-world.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "hello-world.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "hello-world.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /scripts/save-portable-container-image: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Requirements (Fedora): 5 | # sudo dnf install podman jq pigz 6 | 7 | HERE=$(dirname "$(readlink --canonicalize "$BASH_SOURCE")") 8 | . "$HERE/_env" 9 | . "$HERE/_trap" 10 | 11 | if (( "$#" < 2 )); then 12 | echo 'usage: save-portable-container-image [IMAGE NAME] [FILE PATH]' 13 | exit 1 14 | fi 15 | 16 | if command -v podman &> /dev/null; then 17 | CONTAINER_IMAGE_TOOL=podman 18 | else 19 | CONTAINER_IMAGE_TOOL=docker 20 | fi 21 | 22 | if ! command -v "$CONTAINER_IMAGE_TOOL" &> /dev/null; then 23 | m 'missing container image tool: `podman` or `docker`' "$RED" 24 | MISSING=true 25 | fi 26 | 27 | if command -v pigz &> /dev/null; then 28 | GZIP_TOOL=pigz 29 | else 30 | GZIP_TOOL=gzip 31 | fi 32 | 33 | if ! command -v "$GZIP_TOOL" &> /dev/null; then 34 | m 'missing gzip tool: `pigz` or `gzip`' "$RED" 35 | MISSING=true 36 | fi 37 | 38 | if ! command -v tar &> /dev/null; then 39 | m 'missing `tar` tool' "$RED" 40 | MISSING=true 41 | fi 42 | 43 | if ! command -v jq &> /dev/null; then 44 | m 'missing `jq` tool' "$RED" 45 | MISSING=true 46 | fi 47 | 48 | if [ "$MISSING" == true ]; then 49 | exit 1 50 | fi 51 | 52 | IMAGE=$1 53 | FILE=$2 54 | 55 | EXTENSION=${FILE#*.} 56 | if [ "$EXTENSION" != tar.gz ]; then 57 | m 'file extension must be tar.gz' "$RED" 58 | exit 1 59 | fi 60 | 61 | WORK=$(mktemp --directory) 62 | 63 | copy_function goodbye old_goodbye 64 | function goodbye () { 65 | rm --recursive --force "$WORK" 66 | old_goodbye $1 67 | } 68 | 69 | # Save 70 | m "saving $IMAGE..." 71 | "$CONTAINER_IMAGE_TOOL" save "$IMAGE" --output "$WORK/image.tar" 72 | 73 | # Unpack 74 | m "unpacking..." 75 | mkdir --parents "$WORK/image" 76 | tar --extract --file="$WORK/image.tar" --directory="$WORK/image" 77 | 78 | # Fix 79 | cat "$WORK/image/manifest.json" | jq '.[].RepoTags += ["portable"]' > "$WORK/image/manifest.json~" 80 | mv --force "$WORK/image/manifest.json~" "$WORK/image/manifest.json" 81 | 82 | # Repack 83 | m "repacking to $FILE..." 84 | pushd "$WORK/image" > /dev/null 85 | tar --create --use-compress-program="$GZIP_TOOL" --file="$FILE" * 86 | popd > /dev/null 87 | -------------------------------------------------------------------------------- /examples/helm/hello-world/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "hello-world.fullname" . }} 5 | labels: 6 | {{- include "hello-world.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "hello-world.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "hello-world.selectorLabels" . | nindent 8 }} 22 | spec: 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | serviceAccountName: {{ include "hello-world.serviceAccountName" . }} 28 | securityContext: 29 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 30 | containers: 31 | - name: {{ .Chart.Name }} 32 | securityContext: 33 | {{- toYaml .Values.securityContext | nindent 12 }} 34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 35 | imagePullPolicy: {{ .Values.image.pullPolicy }} 36 | ports: 37 | - name: http 38 | containerPort: 8080 39 | protocol: TCP 40 | livenessProbe: 41 | httpGet: 42 | path: / 43 | port: http 44 | readinessProbe: 45 | httpGet: 46 | path: / 47 | port: http 48 | resources: 49 | {{- toYaml .Values.resources | nindent 12 }} 50 | {{- with .Values.nodeSelector }} 51 | nodeSelector: 52 | {{- toYaml . | nindent 8 }} 53 | {{- end }} 54 | {{- with .Values.affinity }} 55 | affinity: 56 | {{- toYaml . | nindent 8 }} 57 | {{- end }} 58 | {{- with .Values.tolerations }} 59 | tolerations: 60 | {{- toYaml . | nindent 8 }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /turandot/commands/template-delist.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/tliron/kutil/util" 6 | ) 7 | 8 | func init() { 9 | templateCommand.AddCommand(templateDelistCommand) 10 | templateDelistCommand.Flags().StringVarP(®istry, "registry", "r", "default", "name of registry") 11 | templateDelistCommand.Flags().BoolVarP(&all, "all", "a", false, "delist all templates") 12 | } 13 | 14 | var templateDelistCommand = &cobra.Command{ 15 | Use: "delist [[SERVICE TEMPLATE NAME]]", 16 | Short: "Delist a service template from a registry (or all service templates)", 17 | Args: cobra.RangeArgs(0, 1), 18 | Run: func(cmd *cobra.Command, args []string) { 19 | if len(args) == 1 { 20 | serviceTemplateName := args[0] 21 | DelistServiceTemplate(serviceTemplateName) 22 | } else if all { 23 | DelistAllTemplates() 24 | } else { 25 | util.Fail("must provide service template name or specify \"--all\"") 26 | } 27 | }, 28 | } 29 | 30 | func DelistServiceTemplate(serviceTemplateName string) { 31 | turandot := NewClient().Turandot() 32 | registry_, err := turandot.Reposure.RegistryClient().Get(namespace, registry) 33 | util.FailOnError(err) 34 | spooler := turandot.Reposure.SurrogateSpoolerClient(registry_) 35 | 36 | imageName := turandot.RegistryImageNameForServiceTemplateName(serviceTemplateName) 37 | err = spooler.DeleteImage(imageName) 38 | util.FailOnError(err) 39 | } 40 | 41 | func DelistAllTemplates() { 42 | turandot := NewClient().Turandot() 43 | registry_, err := turandot.Reposure.RegistryClient().Get(namespace, registry) 44 | util.FailOnError(err) 45 | command, err := turandot.Reposure.SurrogateCommandClient(registry_) 46 | util.FailOnError(err) 47 | imageNames, err := command.ListImages() 48 | util.FailOnError(err) 49 | spooler := turandot.Reposure.SurrogateSpoolerClient(registry_) 50 | 51 | for _, imageName := range imageNames { 52 | if serviceTemplateName, ok := turandot.ServiceTemplateNameForRegistryImageName(imageName); ok { 53 | log.Infof("deleting template: %s", serviceTemplateName) 54 | err := spooler.DeleteImage(imageName) 55 | util.FailOnError(err) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/helm/hello-world/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for hello-world. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: bitnami/nginx 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: true 20 | # Annotations to add to the service account 21 | annotations: {} 22 | # The name of the service account to use. 23 | # If not set and create is true, a name is generated using the fullname template 24 | name: "" 25 | 26 | podAnnotations: {} 27 | 28 | podSecurityContext: {} 29 | # fsGroup: 2000 30 | 31 | securityContext: {} 32 | # capabilities: 33 | # drop: 34 | # - ALL 35 | # readOnlyRootFilesystem: true 36 | # runAsNonRoot: true 37 | # runAsUser: 1000 38 | 39 | service: 40 | type: ClusterIP 41 | port: 80 42 | 43 | ingress: 44 | enabled: false 45 | annotations: {} 46 | # kubernetes.io/ingress.class: nginx 47 | # kubernetes.io/tls-acme: "true" 48 | hosts: 49 | - host: chart-example.local 50 | paths: [] 51 | tls: [] 52 | # - secretName: chart-example-tls 53 | # hosts: 54 | # - chart-example.local 55 | 56 | resources: {} 57 | # We usually recommend not to specify default resources and to leave this as a conscious 58 | # choice for the user. This also increases chances charts run on environments with little 59 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 60 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 61 | # limits: 62 | # cpu: 100m 63 | # memory: 128Mi 64 | # requests: 65 | # cpu: 100m 66 | # memory: 128Mi 67 | 68 | autoscaling: 69 | enabled: false 70 | minReplicas: 1 71 | maxReplicas: 100 72 | targetCPUUtilizationPercentage: 80 73 | # targetMemoryUtilizationPercentage: 80 74 | 75 | nodeSelector: {} 76 | 77 | tolerations: [] 78 | 79 | affinity: {} 80 | -------------------------------------------------------------------------------- /apis/applyconfiguration/turandot.puccini.cloud/v1alpha1/servicetemplateindirect.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | // ServiceTemplateIndirectApplyConfiguration represents an declarative configuration of the ServiceTemplateIndirect type for use 6 | // with apply. 7 | type ServiceTemplateIndirectApplyConfiguration struct { 8 | Namespace *string `json:"namespace,omitempty"` 9 | Registry *string `json:"registry,omitempty"` 10 | Name *string `json:"name,omitempty"` 11 | } 12 | 13 | // ServiceTemplateIndirectApplyConfiguration constructs an declarative configuration of the ServiceTemplateIndirect type for use with 14 | // apply. 15 | func ServiceTemplateIndirect() *ServiceTemplateIndirectApplyConfiguration { 16 | return &ServiceTemplateIndirectApplyConfiguration{} 17 | } 18 | 19 | // WithNamespace sets the Namespace field in the declarative configuration to the given value 20 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 21 | // If called multiple times, the Namespace field is set to the value of the last call. 22 | func (b *ServiceTemplateIndirectApplyConfiguration) WithNamespace(value string) *ServiceTemplateIndirectApplyConfiguration { 23 | b.Namespace = &value 24 | return b 25 | } 26 | 27 | // WithRegistry sets the Registry field in the declarative configuration to the given value 28 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 29 | // If called multiple times, the Registry field is set to the value of the last call. 30 | func (b *ServiceTemplateIndirectApplyConfiguration) WithRegistry(value string) *ServiceTemplateIndirectApplyConfiguration { 31 | b.Registry = &value 32 | return b 33 | } 34 | 35 | // WithName sets the Name field in the declarative configuration to the given value 36 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 37 | // If called multiple times, the Name field is set to the value of the last call. 38 | func (b *ServiceTemplateIndirectApplyConfiguration) WithName(value string) *ServiceTemplateIndirectApplyConfiguration { 39 | b.Name = &value 40 | return b 41 | } 42 | -------------------------------------------------------------------------------- /turandot-profile-generator/parse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/go-openapi/spec" 7 | ) 8 | 9 | const extensionName = "x-kubernetes-group-version-kind" 10 | const defaultGroupPrefix = "k8s.io.api." 11 | const coreGroup = "core" 12 | 13 | func (self *Generator) GetTypeName(schema spec.Schema) string { 14 | name, group, _, kind := getGVK(schema) 15 | if rename, ok := self.Configuration.Rename[name]; ok { 16 | return rename 17 | } else if import_, ok := self.Configuration.Groups.Imports[group]; ok { 18 | return import_.NamespacePrefix + ":" + kind 19 | } else if kind != "" { 20 | return kind 21 | } else { 22 | return name 23 | } 24 | } 25 | 26 | func parseExtension(extension any) (string, string, string) { 27 | extension_ := extension.([]any) 28 | map_ := extension_[0].(map[string]any) 29 | group := fixGroup(map_["group"].(string)) 30 | version := map_["version"].(string) 31 | kind := map_["kind"].(string) 32 | return group, version, kind 33 | } 34 | 35 | func parseGVK(name string) (string, string, string) { 36 | split := strings.Split(name, ".") 37 | length := len(split) 38 | if length < 2 { 39 | return "", "", "" 40 | } 41 | group := fixGroup(strings.Join(split[0:length-2], ".")) 42 | version := split[length-2] 43 | kind := split[length-1] 44 | return group, version, kind 45 | } 46 | 47 | func getGVK(schema spec.Schema) (string, string, string, string) { 48 | if len(schema.Type) > 0 { 49 | return schema.Type[0], "", "", "" 50 | } else { 51 | ref := schema.Ref.GetPointer().String() // example: #/definitions/k8s.io.api.core.v1.LocalObjectReference 52 | split := strings.Split(ref, "/") 53 | name := split[len(split)-1] 54 | group, version, kind := parseGVK(name) 55 | return name, group, version, kind 56 | } 57 | } 58 | 59 | func isGVK(schema spec.Schema, group string, version string, kind string) bool { 60 | _, group_, version_, kind_ := getGVK(schema) 61 | return (group_ == group) && (version_ == version) && (kind_ == kind) 62 | } 63 | 64 | func fixGroup(group string) string { 65 | // In the main Kubernetes API the standard prefix is reversed for some reason 66 | if strings.HasPrefix(group, "io.k8s.") { 67 | group = "k8s.io." + group[len("io.k8s."):] 68 | } 69 | return group 70 | } 71 | -------------------------------------------------------------------------------- /apis/applyconfiguration/turandot.puccini.cloud/v1alpha1/servicenodemodestate.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | import ( 6 | v1alpha1 "github.com/tliron/turandot/resources/turandot.puccini.cloud/v1alpha1" 7 | ) 8 | 9 | // ServiceNodeModeStateApplyConfiguration represents an declarative configuration of the ServiceNodeModeState type for use 10 | // with apply. 11 | type ServiceNodeModeStateApplyConfiguration struct { 12 | Mode *string `json:"mode,omitempty"` 13 | State *v1alpha1.ModeState `json:"state,omitempty"` 14 | Message *string `json:"message,omitempty"` 15 | } 16 | 17 | // ServiceNodeModeStateApplyConfiguration constructs an declarative configuration of the ServiceNodeModeState type for use with 18 | // apply. 19 | func ServiceNodeModeState() *ServiceNodeModeStateApplyConfiguration { 20 | return &ServiceNodeModeStateApplyConfiguration{} 21 | } 22 | 23 | // WithMode sets the Mode field in the declarative configuration to the given value 24 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 25 | // If called multiple times, the Mode field is set to the value of the last call. 26 | func (b *ServiceNodeModeStateApplyConfiguration) WithMode(value string) *ServiceNodeModeStateApplyConfiguration { 27 | b.Mode = &value 28 | return b 29 | } 30 | 31 | // WithState sets the State field in the declarative configuration to the given value 32 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 33 | // If called multiple times, the State field is set to the value of the last call. 34 | func (b *ServiceNodeModeStateApplyConfiguration) WithState(value v1alpha1.ModeState) *ServiceNodeModeStateApplyConfiguration { 35 | b.State = &value 36 | return b 37 | } 38 | 39 | // WithMessage sets the Message field in the declarative configuration to the given value 40 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 41 | // If called multiple times, the Message field is set to the value of the last call. 42 | func (b *ServiceNodeModeStateApplyConfiguration) WithMessage(value string) *ServiceNodeModeStateApplyConfiguration { 43 | b.Message = &value 44 | return b 45 | } 46 | -------------------------------------------------------------------------------- /controller/substitution.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | contextpkg "context" 5 | 6 | "github.com/tliron/exturl" 7 | reposure "github.com/tliron/reposure/resources/reposure.puccini.cloud/v1alpha1" 8 | ) 9 | 10 | func (self *Controller) Substitute(context contextpkg.Context, namespace string, nodeTemplateName string, inputs map[string]any, mode string, site string, urlContext *exturl.Context) error { 11 | // hacky ;) 12 | registryName := "default" 13 | var serviceTemplateName string 14 | switch nodeTemplateName { 15 | case "central-pbx": 16 | serviceTemplateName = "asterisk-vnf" 17 | case "edge-pbx": 18 | serviceTemplateName = "asterisk-cnf" 19 | case "data-plane": 20 | serviceTemplateName = "simple-data-plane" 21 | } 22 | serviceName := serviceTemplateName 23 | 24 | if (site == "") || (site == self.Site) { 25 | // Local 26 | if registry, err := self.Client.Reposure.RegistryClient().Get(namespace, registryName); err == nil { 27 | if _, err := self.Client.CreateServiceFromTemplate(namespace, serviceName, registry, serviceTemplateName, inputs, mode); err != nil { 28 | return err 29 | } 30 | } else { 31 | return err 32 | } 33 | } else { 34 | // Delegate 35 | self.Log.Infof("delegating %q to: %s", serviceTemplateName, site) 36 | if remoteClient, err := self.NewDelegate(site); err == nil { 37 | if err := remoteClient.InstallOperator(site, "docker.io", true); err != nil { 38 | return err 39 | } 40 | if err := remoteClient.Reposure.InstallOperator("docker.io", true); err != nil { 41 | return err 42 | } 43 | 44 | // hack: Minikube 45 | var remoteRegistry *reposure.Registry 46 | if remoteRegistry, err = remoteClient.Reposure.CreateRegistryIndirect(namespace, "default", "kube-system", "registry", 80, "", "", ""); err != nil { 47 | return err 48 | } 49 | 50 | if url, err := remoteClient.GetRegistryServiceTemplateURL(remoteRegistry, serviceTemplateName); err == nil { 51 | if url_, err := urlContext.NewURL(url); err == nil { 52 | if _, err := remoteClient.CreateServiceFromContent(context, namespace, serviceName, remoteRegistry, url_, inputs, mode); err != nil { 53 | return err 54 | } 55 | } else { 56 | return err 57 | } 58 | } else { 59 | return err 60 | } 61 | } else { 62 | return err 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /apis/applyconfiguration/turandot.puccini.cloud/v1alpha1/servicespec.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | // ServiceSpecApplyConfiguration represents an declarative configuration of the ServiceSpec type for use 6 | // with apply. 7 | type ServiceSpecApplyConfiguration struct { 8 | ServiceTemplate *ServiceTemplateApplyConfiguration `json:"serviceTemplate,omitempty"` 9 | Inputs map[string]string `json:"inputs,omitempty"` 10 | Mode *string `json:"mode,omitempty"` 11 | } 12 | 13 | // ServiceSpecApplyConfiguration constructs an declarative configuration of the ServiceSpec type for use with 14 | // apply. 15 | func ServiceSpec() *ServiceSpecApplyConfiguration { 16 | return &ServiceSpecApplyConfiguration{} 17 | } 18 | 19 | // WithServiceTemplate sets the ServiceTemplate field in the declarative configuration to the given value 20 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 21 | // If called multiple times, the ServiceTemplate field is set to the value of the last call. 22 | func (b *ServiceSpecApplyConfiguration) WithServiceTemplate(value *ServiceTemplateApplyConfiguration) *ServiceSpecApplyConfiguration { 23 | b.ServiceTemplate = value 24 | return b 25 | } 26 | 27 | // WithInputs puts the entries into the Inputs field in the declarative configuration 28 | // and returns the receiver, so that objects can be build by chaining "With" function invocations. 29 | // If called multiple times, the entries provided by each call will be put on the Inputs field, 30 | // overwriting an existing map entries in Inputs field with the same key. 31 | func (b *ServiceSpecApplyConfiguration) WithInputs(entries map[string]string) *ServiceSpecApplyConfiguration { 32 | if b.Inputs == nil && len(entries) > 0 { 33 | b.Inputs = make(map[string]string, len(entries)) 34 | } 35 | for k, v := range entries { 36 | b.Inputs[k] = v 37 | } 38 | return b 39 | } 40 | 41 | // WithMode sets the Mode field in the declarative configuration to the given value 42 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 43 | // If called multiple times, the Mode field is set to the value of the last call. 44 | func (b *ServiceSpecApplyConfiguration) WithMode(value string) *ServiceSpecApplyConfiguration { 45 | b.Mode = &value 46 | return b 47 | } 48 | -------------------------------------------------------------------------------- /controller/artifact.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | contextpkg "context" 5 | "fmt" 6 | 7 | "github.com/tliron/exturl" 8 | "github.com/tliron/turandot/controller/parser" 9 | resources "github.com/tliron/turandot/resources/turandot.puccini.cloud/v1alpha1" 10 | ) 11 | 12 | func (self *Controller) publishArtifactsToRegistry(context contextpkg.Context, artifacts parser.KubernetesArtifacts, service *resources.Service, urlContext *exturl.Context) (map[string]string, error) { 13 | if len(artifacts) > 0 { 14 | artifactMappings := make(map[string]string) 15 | 16 | for _, artifact := range artifacts { 17 | if registry, err := self.Client.Reposure.RegistryClient().Get(service.Namespace, artifact.Registry); err == nil { 18 | if err := self.Client.Reposure.RegistryClient().UpdateURLContext(registry, urlContext); err == nil { 19 | // Note: OpenShift registry permissions require the namespace as the repository 20 | imageName := fmt.Sprintf("%s/%s", service.Namespace, artifact.Name) 21 | 22 | if directClient, err := self.Client.Reposure.DirectClient(registry); err == nil { 23 | if url, err := urlContext.NewURL(artifact.SourcePath); err == nil { 24 | self.Log.Infof("publishing image %q at %q on %q", imageName, url.String(), directClient.Host) 25 | if name, err := directClient.PushGzippedTarballFromURL(context, url, imageName); err == nil { 26 | self.Log.Infof("published image %q at %q on %q", imageName, url.String(), directClient.Host) 27 | artifactMappings[artifact.SourcePath] = name 28 | } else { 29 | return nil, err 30 | } 31 | } else { 32 | return nil, err 33 | } 34 | } else { 35 | return nil, err 36 | } 37 | } else { 38 | return nil, err 39 | } 40 | } else { 41 | return nil, err 42 | } 43 | } 44 | 45 | /* 46 | if ips, err := kubernetes.GetServiceIPs(self.Context, self.Kubernetes, service.Namespace, "turandot-registry"); err == nil { 47 | for _, artifact := range artifacts { 48 | if name, err := self.PublishOnRegistry(artifact.Name, artifact.SourcePath, ips, urlContext); err == nil { 49 | artifactMappings[artifact.SourcePath] = name 50 | } else { 51 | return nil, err 52 | } 53 | } 54 | } 55 | */ 56 | 57 | if len(artifactMappings) == 0 { 58 | return nil, nil 59 | } else { 60 | return artifactMappings, nil 61 | } 62 | } else { 63 | return nil, nil 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /turandot/commands/client.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | contextpkg "context" 5 | "fmt" 6 | 7 | kubernetesutil "github.com/tliron/kutil/kubernetes" 8 | "github.com/tliron/kutil/util" 9 | reposurepkg "github.com/tliron/reposure/apis/clientset/versioned" 10 | turandotpkg "github.com/tliron/turandot/apis/clientset/versioned" 11 | clientpkg "github.com/tliron/turandot/client" 12 | "github.com/tliron/turandot/controller" 13 | apiextensionspkg "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 14 | kubernetespkg "k8s.io/client-go/kubernetes" 15 | restpkg "k8s.io/client-go/rest" 16 | ) 17 | 18 | // 19 | // Client 20 | // 21 | 22 | type Client struct { 23 | Config *restpkg.Config 24 | Kubernetes kubernetespkg.Interface 25 | REST restpkg.Interface 26 | Context contextpkg.Context 27 | Namespace string 28 | } 29 | 30 | func NewClient() *Client { 31 | config, err := kubernetesutil.NewConfigFromFlags(masterUrl, kubeconfigPath, kubeconfigContext, log) 32 | util.FailOnError(err) 33 | 34 | kubernetes, err := kubernetespkg.NewForConfig(config) 35 | util.FailOnError(err) 36 | 37 | namespace_ := namespace 38 | if clusterMode { 39 | namespace_ = "" 40 | } else if namespace_ == "" { 41 | if namespace__, ok := kubernetesutil.GetConfiguredNamespace(kubeconfigPath, kubeconfigContext); ok { 42 | namespace_ = namespace__ 43 | } 44 | if namespace_ == "" { 45 | util.Fail("could not discover namespace and \"--namespace\" not provided") 46 | } 47 | } 48 | 49 | return &Client{ 50 | Config: config, 51 | Kubernetes: kubernetes, 52 | REST: kubernetes.CoreV1().RESTClient(), 53 | Context: context, 54 | Namespace: namespace_, 55 | } 56 | } 57 | 58 | func (self *Client) Turandot() *clientpkg.Client { 59 | apiExtensions, err := apiextensionspkg.NewForConfig(self.Config) 60 | util.FailOnError(err) 61 | 62 | turandot, err := turandotpkg.NewForConfig(self.Config) 63 | util.FailOnError(err) 64 | 65 | reposure, err := reposurepkg.NewForConfig(self.Config) 66 | util.FailOnError(err) 67 | 68 | return clientpkg.NewClient( 69 | self.Kubernetes, 70 | apiExtensions, 71 | turandot, 72 | reposure, 73 | self.REST, 74 | self.Config, 75 | self.Context, 76 | clusterMode, 77 | clusterRole, 78 | self.Namespace, 79 | controller.NamePrefix, 80 | controller.PartOf, 81 | controller.ManagedBy, 82 | controller.OperatorImageName, 83 | controller.CacheDirectory, 84 | fmt.Sprintf("%s.client", toolName), 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /turandot/commands/template-register.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | contextpkg "context" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/tliron/exturl" 8 | "github.com/tliron/kutil/util" 9 | ) 10 | 11 | func init() { 12 | templateCommand.AddCommand(templateRegisterCommand) 13 | templateRegisterCommand.Flags().StringVarP(®istry, "registry", "r", "default", "name of registry") 14 | templateRegisterCommand.Flags().StringVarP(&filePath, "file", "f", "", "path to a local CSAR or TOSCA YAML file (will be uploaded)") 15 | templateRegisterCommand.Flags().StringVarP(&directoryPath, "directory", "d", "", "path to a local directory of TOSCA YAML files (will be packed into a CSAR and uploaded)") 16 | } 17 | 18 | var templateRegisterCommand = &cobra.Command{ 19 | Use: "register [SERVICE TEMPLATE NAME]", 20 | Short: "Register a service template in a registry from CSAR or TOSCA YAML content", 21 | Args: cobra.ExactArgs(1), 22 | Run: func(cmd *cobra.Command, args []string) { 23 | serviceTemplateName := args[0] 24 | RegisterServiceTemplate(contextpkg.TODO(), serviceTemplateName) 25 | }, 26 | } 27 | 28 | func RegisterServiceTemplate(context contextpkg.Context, serviceTemplateName string) { 29 | urlContext := exturl.NewContext() 30 | defer urlContext.Release() 31 | 32 | if filePath != "" { 33 | if directoryPath != "" { 34 | registerFailOnlyOneOf() 35 | } 36 | 37 | url, err := urlContext.NewValidFileURL(filePath) 38 | util.FailOnError(err) 39 | registerServiceTemplate(context, serviceTemplateName, url) 40 | } else if directoryPath != "" { 41 | if filePath != "" { 42 | registerFailOnlyOneOf() 43 | } 44 | 45 | // TODO pack directory into CSAR 46 | } else { 47 | url, err := urlContext.ReadToInternalURLFromStdin(context, "yaml") 48 | util.FailOnError(err) 49 | registerServiceTemplate(context, serviceTemplateName, url) 50 | } 51 | } 52 | 53 | func registerServiceTemplate(context contextpkg.Context, serviceTemplateName string, url exturl.URL) { 54 | turandot := NewClient().Turandot() 55 | registry_, err := turandot.Reposure.RegistryClient().Get(namespace, registry) 56 | util.FailOnError(err) 57 | spooler := turandot.Reposure.SurrogateSpoolerClient(registry_) 58 | 59 | imageName := turandot.RegistryImageNameForServiceTemplateName(serviceTemplateName) 60 | err = spooler.PushTarballFromURL(context, imageName, url) 61 | util.FailOnError(err) 62 | } 63 | 64 | func registerFailOnlyOneOf() { 65 | util.Fail("must provide only one of \"--file\" or \"--directory\"") 66 | } 67 | -------------------------------------------------------------------------------- /examples/telephony-network-service/simple-data-plane.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | metadata: 4 | 5 | template_name: Simple Data Plane 6 | template_author: Turandot 7 | 8 | imports: 9 | 10 | - namespace_prefix: ns 11 | file: profiles/network-service/profile.yaml 12 | - namespace_prefix: k8s 13 | file: profiles/kubernetes/1.0/profile.yaml 14 | - namespace_prefix: o11n 15 | file: profiles/orchestration/1.0/profile.yaml 16 | 17 | node_types: 18 | 19 | NetworkAttachmentDefinition: 20 | metadata: 21 | turandot.generateNames: 'false' 22 | capabilities: 23 | metadata: k8s:Metadata 24 | network-attachment-definition: k8s:BridgeNetworkAttachmentDefinition 25 | 26 | topology_template: 27 | 28 | inputs: 29 | 30 | namespace: 31 | type: string 32 | default: workspace 33 | 34 | name: 35 | type: string 36 | default: data-plane 37 | 38 | ip-prefix: 39 | type: string 40 | default: '192.168.2' 41 | 42 | node_templates: 43 | 44 | router: 45 | type: ns:Router 46 | 47 | bridge: 48 | type: NetworkAttachmentDefinition 49 | capabilities: 50 | metadata: 51 | properties: 52 | name: { get_input: name } 53 | namespace: { get_input: namespace } 54 | labels: 55 | app.kubernetes.io/name: { get_input: name } 56 | network-attachment-definition: 57 | properties: 58 | config: 59 | name: { get_input: name } 60 | bridge: { get_input: name } 61 | isDefaultGateway: true 62 | ipMasq: true 63 | promiscMode: true 64 | ipam: 65 | type: host-local 66 | subnet: { concat: [ { get_input: ip-prefix }, '.0/24' ] } 67 | rangeStart: { concat: [ { get_input: ip-prefix }, '.2' ] } 68 | rangeEnd: { concat: [ { get_input: ip-prefix }, '.254' ] } 69 | routes: 70 | - dst: '0.0.0.0/0' 71 | gateway: { concat: [ { get_input: ip-prefix }, '.1' ] } 72 | 73 | substitution_mappings: 74 | 75 | node_type: ns:NetworkPlane 76 | capabilities: 77 | connection: [ router, route ] 78 | 79 | policies: 80 | 81 | # Note: "router" will also inherit the provisioning policy from via substitution 82 | - wan: 83 | type: o11n:Provisioning 84 | properties: 85 | virtualizable: false 86 | targets: 87 | - router 88 | -------------------------------------------------------------------------------- /assets/tosca/profiles/orchestration/1.0/policies.yaml: -------------------------------------------------------------------------------- 1 | tosca_definitions_version: tosca_simple_yaml_1_3 2 | 3 | imports: 4 | 5 | - data.yaml 6 | 7 | policy_types: 8 | 9 | Provisioning: 10 | description: >- 11 | Direction for where and how nodes for a node template are provisioned. 12 | 13 | "Provisioning" refers to either instantiation (creating a new node) or allocation (using an 14 | existing node from an inventory). 15 | 16 | Provisioning policies are "inherited" via node template substitution such that they would 17 | be applied to all node templates within the substituted service template. That substituted 18 | service template may override the inherited policy for specific nodes. 19 | properties: 20 | sites: 21 | description: >- 22 | Nodes will be provisioned on all listed sites. 23 | 24 | Note that a site may have multiple clusters. One or more of them may be selected for 25 | provisioning. 26 | type: list 27 | entry_schema: string 28 | required: false 29 | profile: 30 | description: >- 31 | Multiple provisioning profiles may be available, e.g. multiple service templates for 32 | substitution, multiple physical resources for allocation, etc. 33 | 34 | Setting this property will provide hints for selecting a specific profile. 35 | type: ProvisioningProfile 36 | required: false 37 | substitutable: 38 | description: >- 39 | When true allows the node template to be substituted by another service template, which 40 | must have compatible subtitution mappings. 41 | type: boolean 42 | default: true 43 | instantiable: 44 | description: >- 45 | When true allows nodes to be instantiated. False means that only pre-existing nodes will 46 | be allocated. 47 | type: boolean 48 | default: true 49 | virtualizable: 50 | description: >- 51 | When true allows nodes to be virtual (virtual machines or containers). False means that 52 | only physical nodes (baremetal) can be used. When false the "instantiable" property is 53 | ignored, because physical nodes cannot be instantiated, only allocated. 54 | type: boolean 55 | default: true 56 | substitutionInputs: 57 | description: >- 58 | These values will be used as inputs for substitution. 59 | type: map 60 | entry_schema: string 61 | required: false 62 | -------------------------------------------------------------------------------- /turandot/commands/template-list.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/tliron/exturl" 9 | "github.com/tliron/go-ard" 10 | "github.com/tliron/kutil/terminal" 11 | "github.com/tliron/kutil/util" 12 | ) 13 | 14 | func init() { 15 | templateCommand.AddCommand(templateListCommand) 16 | templateListCommand.Flags().StringVarP(®istry, "registry", "r", "default", "name of registry") 17 | } 18 | 19 | var templateListCommand = &cobra.Command{ 20 | Use: "list", 21 | Short: "List service templates registered in a registry", 22 | Run: func(cmd *cobra.Command, args []string) { 23 | ListServiceTemplates() 24 | }, 25 | } 26 | 27 | func ListServiceTemplates() { 28 | turandot := NewClient().Turandot() 29 | registry_, err := turandot.Reposure.RegistryClient().Get(namespace, registry) 30 | util.FailOnError(err) 31 | command, err := turandot.Reposure.SurrogateCommandClient(registry_) 32 | util.FailOnError(err) 33 | imageNames, err := command.ListImages() 34 | util.FailOnError(err) 35 | 36 | if len(imageNames) == 0 { 37 | return 38 | } 39 | sort.Strings(imageNames) 40 | 41 | switch format { 42 | case "": 43 | urlContext := exturl.NewContext() 44 | defer urlContext.Release() 45 | 46 | table := terminal.NewTable(maxWidth, "Name", "Services") 47 | for _, imageName := range imageNames { 48 | if serviceTemplateName, ok := turandot.ServiceTemplateNameForRegistryImageName(imageName); ok { 49 | services, err := turandot.ListServicesForImageName(registry, imageName, urlContext) 50 | util.FailOnError(err) 51 | sort.Strings(services) 52 | table.Add(serviceTemplateName, strings.Join(services, "\n")) 53 | } 54 | } 55 | table.Print() 56 | 57 | case "bare": 58 | for _, imageName := range imageNames { 59 | if serviceTemplateName, ok := turandot.ServiceTemplateNameForRegistryImageName(imageName); ok { 60 | terminal.Println(serviceTemplateName) 61 | } 62 | } 63 | 64 | default: 65 | urlContext := exturl.NewContext() 66 | defer urlContext.Release() 67 | 68 | list := make(ard.List, 0, len(imageNames)) 69 | for _, imageName := range imageNames { 70 | if serviceTemplateName, ok := turandot.ServiceTemplateNameForRegistryImageName(imageName); ok { 71 | map_ := make(ard.StringMap) 72 | map_["Name"] = serviceTemplateName 73 | map_["Services"], err = turandot.ListServicesForImageName(registry, imageName, urlContext) 74 | util.FailOnError(err) 75 | list = append(list, map_) 76 | } 77 | } 78 | Transcriber().Write(list) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /turandot/commands/service-list.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/tliron/go-ard" 9 | "github.com/tliron/kutil/terminal" 10 | "github.com/tliron/kutil/util" 11 | resources "github.com/tliron/turandot/resources/turandot.puccini.cloud/v1alpha1" 12 | ) 13 | 14 | func init() { 15 | serviceCommand.AddCommand(serviceListCommand) 16 | } 17 | 18 | var serviceListCommand = &cobra.Command{ 19 | Use: "list", 20 | Short: "List deployed services", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | ListServices() 23 | }, 24 | } 25 | 26 | func ListServices() { 27 | services, err := NewClient().Turandot().ListServices() 28 | util.FailOnError(err) 29 | if len(services.Items) == 0 { 30 | return 31 | } 32 | // TODO: sort services by name? they seem already sorted! 33 | 34 | switch format { 35 | case "": 36 | table := terminal.NewTable(maxWidth, "Name", "State", "Mode", "Inputs", "Outputs") 37 | for _, service := range services.Items { 38 | mode := fmt.Sprintf("%s\n", service.Status.Mode) 39 | if service.Status.NodeStates != nil { 40 | for node, nodeState := range service.Status.NodeStates { 41 | if nodeState.Mode == service.Status.Mode { 42 | mode += fmt.Sprintf("%s: %s\n", node, nodeState.State) 43 | } 44 | } 45 | } 46 | 47 | var inputs string 48 | if service.Spec.Inputs != nil { 49 | for _, name := range sortedMapStringStringKeys(service.Spec.Inputs) { 50 | input := service.Spec.Inputs[name] 51 | inputs += fmt.Sprintf("%s: %s\n", name, input) 52 | } 53 | } 54 | 55 | var outputs string 56 | if service.Status.Outputs != nil { 57 | for _, name := range sortedMapStringStringKeys(service.Status.Outputs) { 58 | output := service.Status.Outputs[name] 59 | outputs += fmt.Sprintf("%s: %s\n", name, output) 60 | } 61 | } 62 | 63 | table.Add(service.Name, string(service.Status.InstantiationState), mode, inputs, outputs) 64 | } 65 | table.Print() 66 | 67 | case "bare": 68 | for _, service := range services.Items { 69 | terminal.Println(service.Name) 70 | } 71 | 72 | default: 73 | list := make(ard.List, len(services.Items)) 74 | for index, service := range services.Items { 75 | list[index] = resources.ServiceToARD(&service) 76 | } 77 | Transcriber().Write(list) 78 | } 79 | } 80 | 81 | func sortedMapStringStringKeys(map_ map[string]string) []string { 82 | keys := make([]string, 0, len(map_)) 83 | for key := range map_ { 84 | keys = append(keys, key) 85 | } 86 | sort.Strings(keys) 87 | return keys 88 | } 89 | -------------------------------------------------------------------------------- /apis/clientset/versioned/fake/clientset_generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by client-gen. DO NOT EDIT. 2 | 3 | package fake 4 | 5 | import ( 6 | clientset "github.com/tliron/turandot/apis/clientset/versioned" 7 | turandotv1alpha1 "github.com/tliron/turandot/apis/clientset/versioned/typed/turandot.puccini.cloud/v1alpha1" 8 | faketurandotv1alpha1 "github.com/tliron/turandot/apis/clientset/versioned/typed/turandot.puccini.cloud/v1alpha1/fake" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/watch" 11 | "k8s.io/client-go/discovery" 12 | fakediscovery "k8s.io/client-go/discovery/fake" 13 | "k8s.io/client-go/testing" 14 | ) 15 | 16 | // NewSimpleClientset returns a clientset that will respond with the provided objects. 17 | // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, 18 | // without applying any validations and/or defaults. It shouldn't be considered a replacement 19 | // for a real clientset and is mostly useful in simple unit tests. 20 | func NewSimpleClientset(objects ...runtime.Object) *Clientset { 21 | o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) 22 | for _, obj := range objects { 23 | if err := o.Add(obj); err != nil { 24 | panic(err) 25 | } 26 | } 27 | 28 | cs := &Clientset{tracker: o} 29 | cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} 30 | cs.AddReactor("*", "*", testing.ObjectReaction(o)) 31 | cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { 32 | gvr := action.GetResource() 33 | ns := action.GetNamespace() 34 | watch, err := o.Watch(gvr, ns) 35 | if err != nil { 36 | return false, nil, err 37 | } 38 | return true, watch, nil 39 | }) 40 | 41 | return cs 42 | } 43 | 44 | // Clientset implements clientset.Interface. Meant to be embedded into a 45 | // struct to get a default implementation. This makes faking out just the method 46 | // you want to test easier. 47 | type Clientset struct { 48 | testing.Fake 49 | discovery *fakediscovery.FakeDiscovery 50 | tracker testing.ObjectTracker 51 | } 52 | 53 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 54 | return c.discovery 55 | } 56 | 57 | func (c *Clientset) Tracker() testing.ObjectTracker { 58 | return c.tracker 59 | } 60 | 61 | var ( 62 | _ clientset.Interface = &Clientset{} 63 | _ testing.FakeClient = &Clientset{} 64 | ) 65 | 66 | // TurandotV1alpha1 retrieves the TurandotV1alpha1Client 67 | func (c *Clientset) TurandotV1alpha1() turandotv1alpha1.TurandotV1alpha1Interface { 68 | return &faketurandotv1alpha1.FakeTurandotV1alpha1{Fake: &c.Fake} 69 | } 70 | -------------------------------------------------------------------------------- /controller/parser/kubernetes-resource-mapping.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/tliron/go-transcribe" 5 | "github.com/tliron/kutil/kubernetes" 6 | "github.com/tliron/kutil/util" 7 | "gopkg.in/yaml.v3" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | ) 10 | 11 | // 12 | // KubernetesResourceMapping 13 | // 14 | 15 | type KubernetesResourceMapping struct { 16 | Capability string `yaml:"capability" json:"capability"` 17 | APIVersion string `yaml:"apiVersion" json:"apiVersion"` 18 | Kind string `yaml:"kind" json:"kind"` 19 | Name string `yaml:"name" json:"name"` 20 | Namespace string `yaml:"namespace" json:"namespace"` 21 | AttributeMappings map[string]string `yaml:"attributes,omitempty" json:"attributes,omitempty"` 22 | } 23 | 24 | // 25 | // KubernetesResourceMappingList 26 | // 27 | 28 | type KubernetesResourceMappingList []*KubernetesResourceMapping 29 | 30 | func (self *KubernetesResourceMapping) GVK() (schema.GroupVersionKind, error) { 31 | if gvk, err := kubernetes.ParseGVK(self.APIVersion, self.Kind); err == nil { 32 | return gvk, nil 33 | } else { 34 | return schema.GroupVersionKind{}, err 35 | } 36 | } 37 | 38 | // 39 | // KubernetesResourceMappings 40 | // 41 | 42 | type KubernetesResourceMappings map[string]KubernetesResourceMappingList 43 | 44 | func NewKubernetesResourceMappings() KubernetesResourceMappings { 45 | return make(KubernetesResourceMappings) 46 | } 47 | 48 | func DecodeKubernetesResourceMappings(code string) (KubernetesResourceMappings, bool) { 49 | var self KubernetesResourceMappings 50 | if err := yaml.Unmarshal(util.StringToBytes(code), &self); err == nil { 51 | return self, true 52 | } else { 53 | return nil, false 54 | } 55 | } 56 | 57 | func (self KubernetesResourceMappings) Add(vertexId string, capability string, apiVersion string, kind string, name string, namespace string, attributeMappings map[string]string) { 58 | mapping := &KubernetesResourceMapping{ 59 | Capability: capability, 60 | APIVersion: apiVersion, 61 | Kind: kind, 62 | Name: name, 63 | Namespace: namespace, 64 | AttributeMappings: attributeMappings, 65 | } 66 | self[vertexId] = append(self[vertexId], mapping) 67 | } 68 | 69 | func (self KubernetesResourceMappings) JSON() map[string]string { 70 | map_ := make(map[string]string) 71 | for vertexId, list := range self { 72 | if value, err := transcribe.NewTranscriber().StringifyJSON(list); err == nil { 73 | map_[vertexId] = value 74 | } 75 | } 76 | return map_ 77 | } 78 | -------------------------------------------------------------------------------- /turandot-profile-generator/generator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/go-openapi/loads" 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | // 13 | // Generator 14 | // 15 | 16 | type Generator struct { 17 | ConfigurationPath string 18 | Configuration Configuration 19 | OpenAPI *loads.Document 20 | Nodes Nodes 21 | Writer *os.File 22 | } 23 | 24 | func NewGenerator(configurationPath string) (*Generator, error) { 25 | self := Generator{ConfigurationPath: configurationPath} 26 | if err := self.ReadConfig(); err == nil { 27 | if err = self.ReadOpenAPI(); err == nil { 28 | self.Nodes = self.ReadNodes() 29 | return &self, nil 30 | } else { 31 | return nil, err 32 | } 33 | } else { 34 | return nil, err 35 | } 36 | } 37 | 38 | func (self *Generator) OpenWriter(fileName string) error { 39 | var err error 40 | path := filepath.Join(filepath.Dir(self.ConfigurationPath), self.Configuration.OutputDir, fileName) 41 | if self.Writer, err = os.Create(path); err == nil { 42 | fmt.Println("writing", path) 43 | } 44 | return err 45 | } 46 | 47 | func (self *Generator) CloseWriter() error { 48 | if self.Writer != nil { 49 | return self.Writer.Close() 50 | } else { 51 | return nil 52 | } 53 | } 54 | 55 | func (self *Generator) Generate() error { 56 | capabilityNodes, dataNodes := self.SplitNodes() 57 | 58 | if err := self.GenerateFile("capabilities.yaml", "capability", capabilityNodes); err != nil { 59 | return err 60 | } 61 | 62 | if err := self.GenerateFile("data.yaml", "data", dataNodes); err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func (self *Generator) GenerateFile(fileName string, entity string, nodes Nodes) error { 70 | if err := self.OpenWriter(fileName); err != nil { 71 | return err 72 | } 73 | defer self.CloseWriter() 74 | 75 | self.WriteHeader(entity) 76 | 77 | self.Writeln() 78 | self.WriteKey(0, entity+"_types") 79 | 80 | isCapability := entity == "capability" 81 | for _, node := range nodes { 82 | self.Writeln() 83 | self.WriteType(node, isCapability) 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func (self *Generator) ReadConfig() error { 90 | if bytes, err := os.ReadFile(self.ConfigurationPath); err == nil { 91 | return yaml.Unmarshal(bytes, &self.Configuration) 92 | } else { 93 | return err 94 | } 95 | } 96 | 97 | func (self *Generator) ReadOpenAPI() error { 98 | var err error 99 | self.OpenAPI, err = loads.JSONSpec(self.Configuration.OpenAPI) 100 | return err 101 | } 102 | -------------------------------------------------------------------------------- /turandot-operator/controller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/heptiolabs/healthcheck" 8 | "github.com/tliron/kutil/kubernetes" 9 | "github.com/tliron/kutil/util" 10 | versionpkg "github.com/tliron/kutil/version" 11 | reposurepkg "github.com/tliron/reposure/apis/clientset/versioned" 12 | turandotpkg "github.com/tliron/turandot/apis/clientset/versioned" 13 | controllerpkg "github.com/tliron/turandot/controller" 14 | apiextensionspkg "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 15 | "k8s.io/client-go/dynamic" 16 | kubernetespkg "k8s.io/client-go/kubernetes" 17 | "k8s.io/client-go/tools/clientcmd" 18 | ) 19 | 20 | func Controller() { 21 | if version { 22 | versionpkg.Print() 23 | util.Exit(0) 24 | return 25 | } 26 | 27 | log.Noticef("%s version=%s revision=%s site=%s", toolName, versionpkg.GitVersion, versionpkg.GitRevision, site) 28 | 29 | // Config 30 | 31 | config, err := clientcmd.BuildConfigFromFlags(masterUrl, kubeconfigPath) 32 | util.FailOnError(err) 33 | 34 | if clusterMode { 35 | namespace = "" 36 | } else if namespace == "" { 37 | if namespace_, ok := kubernetes.GetConfiguredNamespace(kubeconfigPath, kubeconfigContext); ok { 38 | namespace = namespace_ 39 | } 40 | if namespace == "" { 41 | namespace = kubernetes.GetServiceAccountNamespace() 42 | } 43 | if namespace == "" { 44 | util.Fail("could not discover namespace and namespace not provided") 45 | } 46 | } 47 | 48 | // Clients 49 | 50 | dynamicClient, err := dynamic.NewForConfig(config) 51 | util.FailOnError(err) 52 | 53 | kubernetesClient, err := kubernetespkg.NewForConfig(config) 54 | util.FailOnError(err) 55 | 56 | apiExtensionsClient, err := apiextensionspkg.NewForConfig(config) 57 | util.FailOnError(err) 58 | 59 | turandotClient, err := turandotpkg.NewForConfig(config) 60 | util.FailOnError(err) 61 | 62 | reposureClient, err := reposurepkg.NewForConfig(config) 63 | util.FailOnError(err) 64 | 65 | // Controller 66 | 67 | controller := controllerpkg.NewController( 68 | context, 69 | toolName, 70 | site, 71 | clusterMode, 72 | clusterRole, 73 | namespace, 74 | dynamicClient, 75 | kubernetesClient, 76 | apiExtensionsClient, 77 | turandotClient, 78 | reposureClient, 79 | config, 80 | cachePath, 81 | resyncPeriod, 82 | util.SetupSignalHandler(), 83 | ) 84 | 85 | // Run 86 | 87 | err = controller.Run(concurrency, func() { 88 | log.Info("starting health monitor") 89 | health := healthcheck.NewHandler() 90 | err := http.ListenAndServe(fmt.Sprintf(":%d", healthPort), health) 91 | util.FailOnError(err) 92 | }) 93 | util.FailOnError(err) 94 | } 95 | -------------------------------------------------------------------------------- /assets/tosca/profiles/helm/1.0/js/kubernetes-resources-get.js: -------------------------------------------------------------------------------- 1 | 2 | const tosca = require('tosca.lib.utils'); 3 | 4 | exports.plugin = function(resources) { 5 | for (let vertexId in clout.vertexes) { 6 | let vertex = clout.vertexes[vertexId]; 7 | if (!tosca.isNodeTemplate(vertex, 'cloud.puccini.helm::Release')) 8 | continue; 9 | let nodeTemplate = vertex.properties; 10 | 11 | let tmp = puccini.temporaryFile('helm-chart-*.tar.gz'); 12 | 13 | try { 14 | puccini.download(nodeTemplate.properties.chart, tmp); 15 | 16 | let name = nodeTemplate.properties.name || nodeTemplate.name; 17 | let namespace = nodeTemplate.properties.namespace; 18 | 19 | let args = [ 20 | 'install', 21 | '--dry-run', 22 | '--output', 'json' 23 | ]; 24 | 25 | if (nodeTemplate.properties.version) 26 | args.push('--version', nodeTemplate.properties.version); 27 | 28 | if (namespace) 29 | args.push('--namespace', namespace); 30 | 31 | if (nodeTemplate.properties.valuesUrl) 32 | args.push('--values', nodeTemplate.properties.valuesUrl); 33 | 34 | let values = nodeTemplate.properties.values; 35 | if (values) 36 | for (let key in values) 37 | args.push('--set-string', puccini.sprintf('%s=%s', key, values[key])); 38 | 39 | args.push(name) 40 | args.push(tmp); 41 | 42 | puccini.log.infof('helm %s', args.join(' ')); 43 | let output = puccini.exec('helm', args); 44 | output = JSON.parse(output); 45 | //puccini.log.infof('%s', JSON.stringify(output, null, ' ')); 46 | if (output.manifest) { 47 | let manifests = puccini.decode(output.manifest, 'yaml', true); 48 | for (let m = 0, l = manifests.length; m < l; m++) { 49 | let manifest = manifests[m]; 50 | processManifest(manifest, name, namespace); 51 | resources.push(manifest); 52 | } 53 | } 54 | if (nodeTemplate.properties.hooks && output.hooks) { 55 | for (let h = 0, l = output.hooks.length; h < l; h++) { 56 | let manifest = puccini.decode(output.hooks[h].manifest, 'yaml'); 57 | processManifest(manifest, name, namespace); 58 | resources.push(manifest); 59 | } 60 | } 61 | } finally { 62 | puccini.exec('rm', ['--force', tmp]); 63 | } 64 | } 65 | }; 66 | 67 | function processManifest(manifest, name, namespace) { 68 | if (!manifest.metadata) 69 | manifest.metadata = {}; 70 | if (!manifest.metadata.annotations) 71 | manifest.metadata.annotations = {}; 72 | if (!manifest.metadata.annotations['meta.helm.sh/release-name']) 73 | manifest.metadata.annotations['meta.helm.sh/release-name'] = name; 74 | if (!manifest.metadata.annotations['meta.helm.sh/release-namespace'] && namespace) 75 | manifest.metadata.annotations['meta.helm.sh/release-namespace'] = namespace; 76 | } 77 | -------------------------------------------------------------------------------- /controller/puccini.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | contextpkg "context" 5 | "errors" 6 | "io" 7 | "strings" 8 | 9 | "github.com/tliron/commonlog" 10 | "github.com/tliron/exturl" 11 | "github.com/tliron/go-ard" 12 | "github.com/tliron/go-transcribe" 13 | "github.com/tliron/kutil/problems" 14 | "github.com/tliron/kutil/terminal" 15 | cloutpkg "github.com/tliron/puccini/clout" 16 | "github.com/tliron/puccini/clout/js" 17 | "github.com/tliron/puccini/tosca/parser" 18 | ) 19 | 20 | var pucciniLog = commonlog.GetLogger("turandot.puccini") 21 | 22 | var pucciniParser = parser.NewParser() 23 | 24 | func CompileTOSCA(context contextpkg.Context, url string, inputs map[string]ard.Value, writer io.Writer, urlContext *exturl.Context) error { 25 | if url_, err := urlContext.NewURL(url); err == nil { 26 | // TODO: bases 27 | 28 | parserContext := pucciniParser.NewContext() 29 | parserContext.URL = url_ 30 | parserContext.Stylist = terminal.NewStylist(false) 31 | parserContext.Inputs = inputs 32 | 33 | if serviceTemplate, err := parserContext.Parse(context); err == nil { 34 | problems := parserContext.GetProblems() 35 | if problems.Empty() { 36 | if clout, err := serviceTemplate.Compile(); err == nil { 37 | return WriteClout(clout, writer) 38 | } else { 39 | return err 40 | } 41 | } else { 42 | return errors.New(problems.ToString(true)) 43 | } 44 | } else { 45 | return err 46 | } 47 | } else { 48 | return err 49 | } 50 | } 51 | 52 | func ReadClout(reader io.Reader, urlContext *exturl.Context) (*cloutpkg.Clout, error) { 53 | if clout, err := cloutpkg.Read(reader, "yaml"); err == nil { 54 | execContext := js.ExecContext{ 55 | Clout: clout, 56 | Problems: problems.NewProblems(nil), 57 | URLContext: urlContext, 58 | Format: "yaml", 59 | } 60 | 61 | if execContext.Resolve(); execContext.Problems.Empty() { 62 | return clout, nil 63 | } else { 64 | return nil, errors.New(execContext.Problems.ToString(true)) 65 | } 66 | } else { 67 | return nil, err 68 | } 69 | } 70 | 71 | func WriteClout(clout *cloutpkg.Clout, writer io.Writer) error { 72 | return transcribe.NewTranscriber().SetWriter(writer).SetIndentSpaces(2).WriteYAML(clout) 73 | } 74 | 75 | func RequireCloutScriptlet(clout *cloutpkg.Clout, scriptletName string, arguments map[string]string, urlContext *exturl.Context) (string, error) { 76 | environment := js.NewEnvironment(scriptletName, pucciniLog, arguments, false, "yaml", false, false, false, "", urlContext) 77 | var builder strings.Builder 78 | environment.Stdout = &builder 79 | if _, err := environment.Require(clout, scriptletName, nil); err == nil { 80 | return builder.String(), nil 81 | } else { 82 | return "", err 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /turandot/commands/root.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os/user" 5 | "path/filepath" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/tliron/commonlog" 9 | "github.com/tliron/kutil/util" 10 | "k8s.io/klog/v2" 11 | ) 12 | 13 | var logTo string 14 | var verbose int 15 | var maxWidth int 16 | var colorize string 17 | var format string 18 | var strict bool 19 | var pretty bool 20 | var base64 bool 21 | 22 | var masterUrl string 23 | var kubeconfigPath string 24 | var kubeconfigContext string 25 | var clusterMode bool 26 | var namespace string 27 | 28 | func init() { 29 | var defaultKubeconfigPath string 30 | if u, err := user.Current(); err == nil { 31 | defaultKubeconfigPath = filepath.Join(u.HomeDir, ".kube", "config") 32 | } 33 | 34 | rootCommand.PersistentFlags().StringVarP(&logTo, "log", "l", "", "log to file (defaults to stderr)") 35 | rootCommand.PersistentFlags().CountVarP(&verbose, "verbose", "v", "add a log verbosity level (can be used twice)") 36 | rootCommand.PersistentFlags().IntVarP(&maxWidth, "width", "j", 0, "maximum output width (0 to use terminal width, -1 for no maximum)") 37 | rootCommand.PersistentFlags().StringVarP(&colorize, "colorize", "z", "true", "colorize output (boolean or \"force\")") 38 | rootCommand.PersistentFlags().StringVarP(&format, "format", "o", "", "output format (\"bare\", \"yaml\", \"json\", \"xjson\", \"xml\", \"cbor\", \"messagepack\", or \"go\")") 39 | rootCommand.PersistentFlags().BoolVarP(&strict, "strict", "y", false, "strict output (for \"yaml\" format only)") 40 | rootCommand.PersistentFlags().BoolVarP(&pretty, "pretty", "p", true, "prettify output") 41 | rootCommand.PersistentFlags().BoolVarP(&base64, "base64", "", false, "output base64 (for \"cbor\", \"messagepack\" formats)") 42 | 43 | rootCommand.PersistentFlags().StringVarP(&masterUrl, "master", "m", "", "address of the Kubernetes API server") 44 | rootCommand.PersistentFlags().StringVarP(&kubeconfigPath, "kubeconfig", "k", defaultKubeconfigPath, "path to Kubernetes configuration") 45 | rootCommand.PersistentFlags().StringVarP(&kubeconfigContext, "context", "x", "", "name of context in Kubernetes configuration") 46 | rootCommand.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "namespace (overrides context namespace in Kubernetes configuration)") 47 | } 48 | 49 | var rootCommand = &cobra.Command{ 50 | Use: toolName, 51 | Short: "Control the Turandot operator", 52 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 53 | util.InitializeColorization(colorize) 54 | commonlog.Initialize(verbose, logTo) 55 | if writer := commonlog.GetWriter(); writer != nil { 56 | klog.SetOutput(writer) 57 | } 58 | }, 59 | } 60 | 61 | func Execute() { 62 | err := rootCommand.Execute() 63 | util.FailOnError(err) 64 | } 65 | -------------------------------------------------------------------------------- /apis/applyconfiguration/turandot.puccini.cloud/v1alpha1/servicetemplatedirect.go: -------------------------------------------------------------------------------- 1 | // Code generated by applyconfiguration-gen. DO NOT EDIT. 2 | 3 | package v1alpha1 4 | 5 | // ServiceTemplateDirectApplyConfiguration represents an declarative configuration of the ServiceTemplateDirect type for use 6 | // with apply. 7 | type ServiceTemplateDirectApplyConfiguration struct { 8 | URL *string `json:"url,omitempty"` 9 | TLSSecret *string `json:"tlsSecret,omitempty"` 10 | TLSSecretDataKey *string `json:"tlsSecretDataKey,omitempty"` 11 | AuthSecret *string `json:"authSecret,omitempty"` 12 | } 13 | 14 | // ServiceTemplateDirectApplyConfiguration constructs an declarative configuration of the ServiceTemplateDirect type for use with 15 | // apply. 16 | func ServiceTemplateDirect() *ServiceTemplateDirectApplyConfiguration { 17 | return &ServiceTemplateDirectApplyConfiguration{} 18 | } 19 | 20 | // WithURL sets the URL field in the declarative configuration to the given value 21 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 22 | // If called multiple times, the URL field is set to the value of the last call. 23 | func (b *ServiceTemplateDirectApplyConfiguration) WithURL(value string) *ServiceTemplateDirectApplyConfiguration { 24 | b.URL = &value 25 | return b 26 | } 27 | 28 | // WithTLSSecret sets the TLSSecret field in the declarative configuration to the given value 29 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 30 | // If called multiple times, the TLSSecret field is set to the value of the last call. 31 | func (b *ServiceTemplateDirectApplyConfiguration) WithTLSSecret(value string) *ServiceTemplateDirectApplyConfiguration { 32 | b.TLSSecret = &value 33 | return b 34 | } 35 | 36 | // WithTLSSecretDataKey sets the TLSSecretDataKey field in the declarative configuration to the given value 37 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 38 | // If called multiple times, the TLSSecretDataKey field is set to the value of the last call. 39 | func (b *ServiceTemplateDirectApplyConfiguration) WithTLSSecretDataKey(value string) *ServiceTemplateDirectApplyConfiguration { 40 | b.TLSSecretDataKey = &value 41 | return b 42 | } 43 | 44 | // WithAuthSecret sets the AuthSecret field in the declarative configuration to the given value 45 | // and returns the receiver, so that objects can be built by chaining "With" function invocations. 46 | // If called multiple times, the AuthSecret field is set to the value of the last call. 47 | func (b *ServiceTemplateDirectApplyConfiguration) WithAuthSecret(value string) *ServiceTemplateDirectApplyConfiguration { 48 | b.AuthSecret = &value 49 | return b 50 | } 51 | -------------------------------------------------------------------------------- /assets/kubernetes/custom-resource-definition.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | 4 | metadata: 5 | name: services.turandot.puccini.cloud 6 | 7 | spec: 8 | group: turandot.puccini.cloud 9 | names: 10 | singular: service 11 | plural: services 12 | kind: Service 13 | listKind: ServiceList 14 | shortNames: 15 | - si # = ServIce? Service Instance? 16 | categories: 17 | - all # will appear in "kubectl get all" 18 | scope: Namespaced 19 | versions: 20 | - name: v1alpha1 21 | served: true 22 | storage: true # one and only one version must be marked with storage=true 23 | subresources: # requires CustomResourceSubresources feature gate enabled 24 | status: {} 25 | schema: 26 | openAPIV3Schema: 27 | type: object 28 | required: [ spec ] 29 | properties: 30 | spec: 31 | type: object 32 | required: [ serviceTemplateUrl ] 33 | properties: 34 | serviceTemplateUrl: 35 | type: string 36 | inputs: 37 | type: object 38 | nullable: true 39 | additionalProperties: 40 | type: string 41 | mode: 42 | type: string 43 | status: 44 | type: object 45 | properties: 46 | cloutPath: 47 | type: string 48 | cloutHash: 49 | type: string 50 | serviceTemplateUrl: 51 | type: string 52 | inputs: 53 | type: object 54 | nullable: true 55 | additionalProperties: 56 | type: string 57 | outputs: 58 | type: object 59 | nullable: true 60 | additionalProperties: 61 | type: string 62 | instantiationState: 63 | type: string 64 | enum: 65 | - NotInstantiated 66 | - Instantiating 67 | - Instantiated 68 | nodeStates: 69 | type: object 70 | nullable: true 71 | additionalProperties: 72 | type: object 73 | properties: 74 | mode: 75 | type: string 76 | state: 77 | type: string 78 | enum: 79 | - Accepted 80 | - Rejected 81 | - Achieved 82 | - Failed 83 | message: 84 | type: string 85 | mode: 86 | type: string 87 | -------------------------------------------------------------------------------- /examples/telephony-network-service/artifacts/keypairs/admin@asterisk-vnf: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAYEA6VeMyKiTXA/dOYoD/xIuJ8KPPiwnSBoAmFBDAY6OiSE8yIVUSoac 4 | I72FtvAyUOJr30R5MshpYMtiSm6zly2MqqBc8RPfVUYvncCNFFr0Nen0SITmAu19dNwcmH 5 | 8a6aZdi9u+bfQ8WB7265THyzOPdBChv3Vh3sh8n+UlNOOUuqxGV6XSVJw+Qj6xUnwMXrpq 6 | XBxqkzfBa81nH1/eO7ADhEwh1T7kzEJQr1hzLa7HhjIhA+jOXha4QKL8Jza60kbAlkhJOI 7 | 8hzWNOkkCGr6HrMnpmoRsSFwQu0m4hV8l6OQEOR08GyzTHKh4WtKb+aGbfECinXUQ98YUz 8 | YdTHB9pnWsAtTbBYaIriljQoIcB5kfxBzdNbDCsdtZiduPh2PhhskW8YE8sHxCc+UNNCWd 9 | 6xeCs9F+BJ3a9PfCVngu0OY/76ZM9oG5Rleovb5DMAa9cmNA2wSPEGiOQTOoNj4fiBVDay 10 | 9S1VmLFM2XqSTJOmE1LOvg4ccUuQ95v350y3M8WbAAAFkBz64Vsc+uFbAAAAB3NzaC1yc2 11 | EAAAGBAOlXjMiok1wP3TmKA/8SLifCjz4sJ0gaAJhQQwGOjokhPMiFVEqGnCO9hbbwMlDi 12 | a99EeTLIaWDLYkpus5ctjKqgXPET31VGL53AjRRa9DXp9EiE5gLtfXTcHJh/GummXYvbvm 13 | 30PFge9uuUx8szj3QQob91Yd7IfJ/lJTTjlLqsRlel0lScPkI+sVJ8DF66alwcapM3wWvN 14 | Zx9f3juwA4RMIdU+5MxCUK9Ycy2ux4YyIQPozl4WuECi/Cc2utJGwJZISTiPIc1jTpJAhq 15 | +h6zJ6ZqEbEhcELtJuIVfJejkBDkdPBss0xyoeFrSm/mhm3xAop11EPfGFM2HUxwfaZ1rA 16 | LU2wWGiK4pY0KCHAeZH8Qc3TWwwrHbWYnbj4dj4YbJFvGBPLB8QnPlDTQlnesXgrPRfgSd 17 | 2vT3wlZ4LtDmP++mTPaBuUZXqL2+QzAGvXJjQNsEjxBojkEzqDY+H4gVQ2svUtVZixTNl6 18 | kkyTphNSzr4OHHFLkPeb9+dMtzPFmwAAAAMBAAEAAAGAF6496GNLSS2G+v7ptuomavyQwv 19 | OCLQwOgWar1i2cg4gu/f6h9kpA8FOcjVtFD/ZakjcFwcSbBSq+1+TyLuZ9fxd1NzYYFTRh 20 | QrY22xj77bjHhNCP8z4c3E42TLehiJLMrZPoeIov0ZWa43+e4x1hcpr5f0+vVgestiFwLD 21 | TodCVloiF+/f05SEnKfx99teNfRefQXQ5sf79BtupoyE/tm3CKgnS2jZAxVgo47irtrs3J 22 | kfb/cocFvfGMe4JPZWWuli+Dy+ZX0/Zvg3cj/3uPn5El6p/KdxEOR1zjXHgwtmH9FFizfG 23 | xjfe6hnR1T8AZrltY/YsMMmd+rL4HDa9U5TZtPznzCQZDpKwtO1xFq3eS7Z3yIZPkxQmYq 24 | a9ViOfeQpxoIXFMfHApabngZJBse2or2fzXR+Q/siwhj0Uvg2PCXJAIMIHdWG58tfe8fwI 25 | J8METxJf+sItxfrC2p6DdIjQ4X7AaUvZGchvmLGqkTBIdutxGc6fnvnAcNCLftozFhAAAA 26 | wA438WVCw2OMMhShrjeRaODKd7vrQTv0P3WnLld36TC+j6qaDE5nZJn/okjckx6l6oMN7Y 27 | ScIYKyCJyu+XDk73skMG1sUw6nZzmekE6vlgkJ/v5MB+dawmoW02j88TcT1mQoVCvjmrrS 28 | kZN5MVm93yXrIHFPC8ah9hO/MUBnraOIUa7W8NrcJujsIob3VGmF8GW7Al7V0IB1GGsAkL 29 | XXeZEEwjPt15U0342MLUlDi0lALZ6dPK9GjYL5KwIb3N7K8AAAAMEA/GuPyylTnBTkv+6f 30 | GPD1t3/nTlSrtmaZbBmuT7OmUvAv5TnhFLnVEk1wA25gH1AhSmEfS0G3eIGwqgnwrMC5Lv 31 | NfDOpr6cDl0skxAAQdfkcY57LVyckAQL4j9leLH2r4/oGJASL3W/PKyaBW9eSIgiqDpAl9 32 | oxxS/2rJtOmvigEJd8PjCZ3RuNlBt46bE/XoEgmyVswkRSSDQ001ZkG5mOzRnOKqr4wcrm 33 | T7VqIME2A3E2Xgo1ySp7aOemqLfpCrAAAAwQDsprkQoVxTecYWRNaq1rbGRk4Qd5tEzi8J 34 | mZa8qUcUHKreuR+C1LVrcza7uzbi9bs46SAHwTFpaimROhPXNQq1axAAoFQZXDkKh6Urz3 35 | QVQ0FLIN8dcsXAn/BFvA1JLdTt2sH7Y8Prlu5RytFc9jxLzhuijePKG+l5f5PsvYE7AnD/ 36 | yFlJaPy4wcNa2HJxsqRzL9riI5HqH3XFWoNl2BVak2Xc6RCYw37lru6/5XNyl+ZIiEmF2M 37 | MJx5RQBGAC/tEAAAAVZW1ibGVtcGFyYWRlQGZvdWNhdWx0AQIDBAUG 38 | -----END OPENSSH PRIVATE KEY----- 39 | --------------------------------------------------------------------------------