Repository: score-spec/score-compose (go)

Issue:
[feature request] Add a `description` field for the provisioners
To improve how PEs document their provisioners and expose them to their Devs, it would be better to have a `description` field in the provisioners files.

Today, if we add this field in the provisioners files, we get this error:
```
Error: failed to save provisioner 1: invalid provisioners file: 0: cmd://python3#dotenv: failed to parse: yaml: unmarshal errors:
  line 9: field description not found in type cmdprov.Provisioner
```

Rough list of things to do to implement this:
- Support this field in the `cmdprov.Provisioner` and `templateprov.Provisioner` and other places where needed
- Update all the current default provisioners with their description
- Update `score-compose provisioners list` to display by default this new `description` field

This field should be optional, to avoid any breaking changes with existing/current provisioners.

Note: here is an example of how this could be used: https://github.com/score-spec/community-provisioners/pull/8

Pull request:
feat: add description field support in provisioners
<!--- Provide a general summary of your changes in the Title above -->

#### Description
<!--- Describe your changes in detail -->

Fix #269: Add description field support for the provisioners

- [x] Support this field in the cmdprov.Provisioner and templateprov.Provisioner and other places where needed
- [x] Update all the current default provisioners with their description
- [x] Update score-compose provisioners list to display by default this new description field

#### What does this PR do?
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->



#### Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [x] New chore (expected functionality to be implemented)

#### Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] My change requires a change to the documentation.
  - [ ] I have updated the documentation accordingly.
- [xI've signed off with an email address that matches the commit author.


Patch:
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 135dd4e..d26a7a2 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -68,6 +68,6 @@ jobs:
       # Upload the results to GitHub's code scanning dashboard (optional).
       # Commenting out will disable upload of results to your repo's Code Scanning dashboard
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
+        uses: github/codeql-action/upload-sarif@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
         with:
           sarif_file: results.sarif
diff --git a/go.mod b/go.mod
index 106ade9..f6dcedb 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@ require (
 	github.com/compose-spec/compose-go/v2 v2.4.8
 	github.com/mitchellh/mapstructure v1.5.0
 	github.com/olekukonko/tablewriter v0.0.5
-	github.com/score-spec/score-go v1.9.3
+	github.com/score-spec/score-go v1.9.5
 	github.com/spf13/cobra v1.9.1
 	github.com/spf13/pflag v1.0.6
 	github.com/stretchr/testify v1.10.0
@@ -51,4 +51,4 @@ require (
 	golang.org/x/sys v0.31.0 // indirect
 	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 	oras.land/oras-go/v2 v2.5.0 // indirect
-)
+)
\ No newline at end of file
diff --git a/go.sum b/go.sum
index 0ee8685..1fb99b3 100644
--- a/go.sum
+++ b/go.sum
@@ -61,8 +61,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
-github.com/score-spec/score-go v1.9.3 h1:YZ1M5MCn/FI6nQNI0RUC0l8tgSwceOg+P7YvvNUyaNE=
-github.com/score-spec/score-go v1.9.3/go.mod h1:FTajUIhA1MyQeYtuTwVVisf+oqf+tV0V44g54hnnpMo=
+github.com/score-spec/score-go v1.9.5 h1:BkZCCf0s4YVYWR6WL4D57P5PT9XxVmfWpNw/rC01750=
+github.com/score-spec/score-go v1.9.5/go.mod h1:hbWRzCHVEyxZP2UhD2nkhTEMRFZZob/uczCnUDl0enI=
 github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
 github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
diff --git a/internal/command/default.provisioners.yaml b/internal/command/default.provisioners.yaml
index af97a96..7eef068 100644
--- a/internal/command/default.provisioners.yaml
+++ b/internal/command/default.provisioners.yaml
@@ -9,6 +9,7 @@
   # By default, match all classes and ids of volume. If you want to override this, create another provisioner definition
   # with a higher priority.
   type: volume
+  description: Creates a persistent volume that can be mounted on a workload
   init: |
     randomVolumeName: {{ .Id | replace "." "-" }}-{{ randAlphaNum 6 }}
   # Store the random volume name if we haven't chosen one yet, otherwise use the one that exists already
@@ -30,11 +31,13 @@
     - source
     - type
 
+
 # The default provisioner for service resources, this expects a workload and port name and will return the hostname and
 # port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency
 # relationship yet.
 - uri: template://default-provisioners/service-port
   type: service-port
+  description: Outputs a hostname and port for connecting to another workload
   outputs: |
     {{ if not .Params.workload }}{{ fail "expected 'workload' param for the target workload name" }}{{ end }}
     {{ if not .Params.port }}{{ fail "expected 'port' param for the name of the target workload service port" }}{{ end }}
@@ -56,6 +59,7 @@
   # By default, match all redis types regardless of class and id. If you want to override this, create another
   # provisioner definition with a higher priority.
   type: redis
+  description: Provisions a dedicated redis instance
   # Init template has the default port and a random service name and password if needed later
   init: |
     port: 6379
@@ -118,6 +122,7 @@
   # By default, match all redis types regardless of class and id. If you want to override this, create another
   # provisioner definition with a higher priority.
   type: postgres
+  description: Provisions a dedicated database on a shared postgres instance
   # Init template has the random service name and password if needed later
   init: |
     randomServiceName: pg-{{ randAlphaNum 6 }}
@@ -217,6 +222,7 @@
 # connectivity.
 - uri: template://default-provisioners/s3
   type: s3
+  description: Provisions a dedicated S3 bucket with AWS-style credentials on a shared minio instance
   # The init template contains some initial seed data that can be used it needed.
   init: |
     randomServiceName: minio-{{ randAlphaNum 6 }}
@@ -318,6 +324,7 @@
 # The default AMQP provisioner provides a simple rabbitmq instance with default configuration and plugins.
 - uri: template://default-provisioners/rabbitmq
   type: amqp
+  description: Provisions a dedicated rabbitmq vhost on a shared instance
   init: |
     randomServiceName: rabbitmq-{{ randAlphaNum 6 }}
     randomVHost: vhost-{{ randAlpha 8 }}
@@ -410,6 +417,7 @@
 # can be replaced by a new provisioner in the future.
 - uri: template://default-provisioners/dns
   type: dns
+  description: Outputs a *.localhost domain as the hostname
   init: |
     randomHostname: dns{{ randAlphaNum 6 | lower }}.localhost
   state: |
@@ -423,6 +431,7 @@
 # It assumes the hostnames and routes provided have no overlaps. Weird behavior may happen if there are overlaps.
 - uri: template://default-provisioners/route
   type: route
+  description: Provisions a ingress route on a shared nginx instance
   init: |
     randomServiceName: routing-{{ randAlphaNum 6 }}
     sk: default-provisioners-routing-instance
@@ -528,6 +537,7 @@
 # The default mongodb provisioner adds a mongodb service to the project which returns a host, port, username, and password, and connection string.
 - uri: template://default-provisioners/mongodb
   type: mongodb
+  description: Provisions a dedicated mongodb database
   init: |
     port: 27017
     randomServiceName: mongo-{{ randAlphaNum 6 }}
@@ -585,6 +595,7 @@
   # By default, match all mysql types regardless of class and id. If you want to override this, create another
   # provisioner definition with a higher priority.
   type: mysql
+  description: Provisions a dedicated mysql database on a shared instance
   # Init template has the random service name and password if needed later
   init: |
     randomServiceName: mysql-{{ randAlphaNum 6 }}
@@ -668,6 +679,7 @@
 
 - uri: template://default-provisioners/kafka-topic
   type: kafka-topic
+  description: Provisions a dedicated kafka topic on a shared kafka broker
   init: |
     brokerPort: 9092
     ctrlPort: 9093
@@ -732,6 +744,7 @@
   # By default, match all elasticsearch types regardless of class and id.
   # If you want to override this, create another provisioner definition with a higher priority.
   type: elasticsearch
+  description: Provisions a dedicated elastic search instance
   # Init template has the random service name and password if needed later
   init: |
     serviceName: elasticsearch
@@ -866,6 +879,7 @@
 
 - uri: template://default-provisioners/mssql
   type: mssql
+  description: Provisions a dedicated database on a shared SQL server instance
   init: |
     randomPassword: {{ randAlphaNum 16 | quote }}
     sk: default-provisioners-mssql
@@ -886,7 +900,6 @@
   volumes: |
     {{ .Init.sk }}-data:
       driver: local
-  # Create an services with the database itself
   services: |
     {{ .Init.sk }}:
       image: mcr.microsoft.com/mssql/server:latest
diff --git a/internal/command/fixtures/provisioners.custom.golden b/internal/command/fixtures/provisioners.custom.golden
index 258fa4e..159ebb9 100644
--- a/internal/command/fixtures/provisioners.custom.golden
+++ b/internal/command/fixtures/provisioners.custom.golden
@@ -137,4 +137,11 @@
     - p1
     - po1
     - pst1
-    - psh1
\ No newline at end of file
+    - psh1
+
+- uri: template://test-provisioners/without-class-without-params-with-description
+  type: without-class-without-params
+  expected_outputs:
+    - o1
+    - o2
+  description: without-class-without-params-with-description
\ No newline at end of file
diff --git a/internal/command/init.go b/internal/command/init.go
index 2e7e218..53e5b2d 100644
--- a/internal/command/init.go
+++ b/internal/command/init.go
@@ -203,7 +203,15 @@ the new provisioners will take precedence.
 				if err != nil {
 					return fmt.Errorf("failed to load provisioner %d: %w", i+1, err)
 				}
-				if err := loader.SaveProvisionerToDirectory(sd.Path, vi, data); err != nil {
+
+				var saveFilename string
+				if vi == "-" {
+					saveFilename = "from-stdin.provisioners.yaml"
+				} else {
+					saveFilename = vi
+				}
+
+				if err := loader.SaveProvisionerToDirectory(sd.Path, saveFilename, data); err != nil {
 					return fmt.Errorf("failed to save provisioner %d: %w", i+1, err)
 				}
 			}
@@ -230,7 +238,9 @@ func init() {
 		"- HTTPS       : https://host/file\n"+
 		"- Git (SSH)   : git-ssh://git@host/repo.git/file\n"+
 		"- Git (HTTPS) : git-https://host/repo.git/file\n"+
-		"- OCI         : oci://[registry/][namespace/]repository[:tag|@digest][#file]")
+		"- OCI         : oci://[registry/][namespace/]repository[:tag|@digest][#file]\n"+
+		"- Local File  : /path/to/local/file\n"+
+		"- Stdin       : - (read from standard input)")
 
 	rootCmd.AddCommand(initCmd)
 }
diff --git a/internal/command/provisioners.go b/internal/command/provisioners.go
index b01e588..eb99b85 100644
--- a/internal/command/provisioners.go
+++ b/internal/command/provisioners.go
@@ -78,18 +78,20 @@ func displayProvisioners(loadedProvisioners []provisioners.Provisioner, outputFo
 	switch outputFormat {
 	case "json":
 		type jsonData struct {
-			Type    string
-			Class   string
-			Params  []string
-			Outputs []string
+			Type        string
+			Class       string
+			Params      []string
+			Outputs     []string
+			Description string
 		}
 		var outputs []jsonData
 		for _, provisioner := range sortedProvisioners {
 			outputs = append(outputs, jsonData{
-				Type:    provisioner.Type(),
-				Class:   provisioner.Class(),
-				Params:  provisioner.Params(),
-				Outputs: provisioner.Outputs(),
+				Type:        provisioner.Type(),
+				Class:       provisioner.Class(),
+				Params:      provisioner.Params(),
+				Outputs:     provisioner.Outputs(),
+				Description: provisioner.Description(),
 			})
 		}
 		outputFormatter = &util.JSONOutputFormatter[[]jsonData]{Data: outputs}
@@ -97,9 +99,9 @@ func displayProvisioners(loadedProvisioners []provisioners.Provisioner, outputFo
 		rows := [][]string{}
 
 		for _, provisioner := range sortedProvisioners {
-			rows = append(rows, []string{provisioner.Type(), provisioner.Class(), strings.Join(provisioner.Params(), ", "), strings.Join(provisioner.Outputs(), ", ")})
+			rows = append(rows, []string{provisioner.Type(), provisioner.Class(), strings.Join(provisioner.Params(), ", "), strings.Join(provisioner.Outputs(), ", "), provisioner.Description()})
 		}
-		headers := []string{"Type", "Class", "Params", "Outputs"}
+		headers := []string{"Type", "Class", "Params", "Outputs", "Description"}
 		outputFormatter = &util.TableOutputFormatter{
 			Headers: headers,
 			Rows:    rows,
diff --git a/internal/command/run.go b/internal/command/run.go
index a5d7feb..b4acdec 100644
--- a/internal/command/run.go
+++ b/internal/command/run.go
@@ -358,3 +358,7 @@ func (l *legacyVolumeProvisioner) Class() string {
 func (l *legacyVolumeProvisioner) Type() string {
 	return l.MatchResourceUid.Type()
 }
+
+func (l *legacyVolumeProvisioner) Description() string {
+	return ""
+}
diff --git a/internal/provisioners/cmdprov/commandprov.go b/internal/provisioners/cmdprov/commandprov.go
index 0571669..9fa97b6 100644
--- a/internal/provisioners/cmdprov/commandprov.go
+++ b/internal/provisioners/cmdprov/commandprov.go
@@ -41,6 +41,11 @@ type Provisioner struct {
 	Args           []string `yaml:"args"`
 	ResourceParams []string `yaml:"params,omitempty"`
 	ResOutputs     []string `yaml:"outputs,omitempty"`
+	ResDescription string   `yaml:"description,omitempty"`
+}
+
+func (p *Provisioner) Description() string {
+	return p.ResDescription
 }
 
 func (p *Provisioner) Uri() string {
diff --git a/internal/provisioners/core.go b/internal/provisioners/core.go
index 486c82b..e77a931 100644
--- a/internal/provisioners/core.go
+++ b/internal/provisioners/core.go
@@ -105,16 +105,22 @@ type Provisioner interface {
 	Type() string
 	Params() []string
 	Outputs() []string
+	Description() string
 }
 
 type ephemeralProvisioner struct {
-	uri       string
-	matchUid  framework.ResourceUid
-	provision func(ctx context.Context, input *Input) (*ProvisionOutput, error)
-	class     string
-	eType     string
-	params    []string
-	outputs   []string
+	uri         string
+	matchUid    framework.ResourceUid
+	provision   func(ctx context.Context, input *Input) (*ProvisionOutput, error)
+	class       string
+	eType       string
+	params      []string
+	outputs     []string
+	description string
+}
+
+func (e *ephemeralProvisioner) Description() string {
+	return e.description
 }
 
 func (e *ephemeralProvisioner) Uri() string {
diff --git a/internal/provisioners/envprov/envprov.go b/internal/provisioners/envprov/envprov.go
index 18d6199..bbef9e7 100644
--- a/internal/provisioners/envprov/envprov.go
+++ b/internal/provisioners/envprov/envprov.go
@@ -103,11 +103,16 @@ func (e *Provisioner) GenerateSubProvisioner(resName string, resUid framework.Re
 // envVarResourceTracker is a child object of EnvVarTracker and is used as a fallback behavior for resource types
 // that are not supported natively: we treat them like environment variables instead with a prefix of the resource name.
 type envVarResourceTracker struct {
-	uid      framework.ResourceUid
-	prefix   string
-	inner    *Provisioner
-	resClass string
-	resType  string
+	uid            framework.ResourceUid
+	prefix         string
+	inner          *Provisioner
+	resClass       string
+	resType        string
+	resDescription string
+}
+
+func (e *envVarResourceTracker) Description() string {
+	return e.resDescription
 }
 
 func (e *envVarResourceTracker) Uri() string {
@@ -172,5 +177,9 @@ func (e *envVarResourceTracker) Params() []string {
 	return nil
 }
 
+func (p *Provisioner) Description() string {
+	return p.Description()
+}
+
 var _ provisioners.Provisioner = (*Provisioner)(nil)
 var _ provisioners.Provisioner = (*envVarResourceTracker)(nil)
diff --git a/internal/provisioners/templateprov/template.go b/internal/provisioners/templateprov/template.go
index 1a74d58..a0e6a39 100644
--- a/internal/provisioners/templateprov/template.go
+++ b/internal/provisioners/templateprov/template.go
@@ -42,6 +42,7 @@ type Provisioner struct {
 	ResType        string  `yaml:"type"`
 	ResClass       *string `yaml:"class,omitempty"`
 	ResId          *string `yaml:"id,omitempty"`
+	ResDescription string  `yaml:"description,omitempty"`
 
 	// The InitTemplate is always evaluated first, it is used as temporary or working set data that may be needed in the
 	// later templates. It has access to the resource inputs and previous state.
@@ -101,6 +102,10 @@ func (p *Provisioner) Uri() string {
 	return p.ProvisionerUri
 }
 
+func (p *Provisioner) Description() string {
+	return p.ResDescription
+}
+
 func (p *Provisioner) Class() string {
 	if p.ResClass == nil {
 		return "(any)"


Test patch:
diff --git a/internal/command/init_test.go b/internal/command/init_test.go
index 7571921..b23adf4 100644
--- a/internal/command/init_test.go
+++ b/internal/command/init_test.go
@@ -76,6 +76,8 @@ Flags:
                                    - Git (SSH)   : git-ssh://git@host/repo.git/file
                                    - Git (HTTPS) : git-https://host/repo.git/file
                                    - OCI         : oci://[registry/][namespace/]repository[:tag|@digest][#file]
+                                   - Local File  : /path/to/local/file
+                                   - Stdin       : - (read from standard input)
 
 Global Flags:
       --quiet           Mute any logging output
diff --git a/internal/command/testdata/provisioners.list.valid.json.golden b/internal/command/testdata/provisioners.list.valid.json.golden
index 0431a5a..1800155 100644
--- a/internal/command/testdata/provisioners.list.valid.json.golden
+++ b/internal/command/testdata/provisioners.list.valid.json.golden
@@ -9,7 +9,8 @@
     "Outputs": [
       "o1",
       "o2"
-    ]
+    ],
+    "Description": ""
   },
   {
     "Type": "with-class-with-params-in-shared",
@@ -21,7 +22,8 @@
     "Outputs": [
       "o1",
       "o2"
-    ]
+    ],
+    "Description": ""
   },
   {
     "Type": "with-class-with-params-in-shared-outputs",
@@ -34,7 +36,8 @@
     "Outputs": [
       "o1",
       "o2"
-    ]
+    ],
+    "Description": ""
   },
   {
     "Type": "with-class-with-params-in-state",
@@ -46,7 +49,8 @@
     "Outputs": [
       "o1",
       "o2"
-    ]
+    ],
+    "Description": ""
   },
   {
     "Type": "with-class-with-params-in-state-outputs",
@@ -59,7 +63,8 @@
     "Outputs": [
       "o1",
       "o2"
-    ]
+    ],
+    "Description": ""
   },
   {
     "Type": "with-class-with-params-in-state-outputs-shared",
@@ -73,7 +78,8 @@
     "Outputs": [
       "o1",
       "o2"
-    ]
+    ],
+    "Description": ""
   },
   {
     "Type": "with-class-without-params",
@@ -82,7 +88,8 @@
     "Outputs": [
       "o1",
       "o2"
-    ]
+    ],
+    "Description": ""
   },
   {
     "Type": "without-class-with-params-in-outputs",
@@ -94,7 +101,8 @@
     "Outputs": [
       "o1",
       "o2"
-    ]
+    ],
+    "Description": ""
   },
   {
     "Type": "without-class-with-params-in-shared",
@@ -106,7 +114,8 @@
     "Outputs": [
       "o1",
       "o2"
-    ]
+    ],
+    "Description": ""
   },
   {
     "Type": "without-class-with-params-in-shared-outputs",
@@ -119,7 +128,8 @@
     "Outputs": [
       "o1",
       "o2"
-    ]
+    ],
+    "Description": ""
   },
   {
     "Type": "without-class-with-params-in-state",
@@ -131,7 +141,8 @@
     "Outputs": [
       "o1",
       "o2"
-    ]
+    ],
+    "Description": ""
   },
   {
     "Type": "without-class-with-params-in-state-outputs",
@@ -144,7 +155,8 @@
     "Outputs": [
       "o1",
       "o2"
-    ]
+    ],
+    "Description": ""
   },
   {
     "Type": "without-class-with-params-in-state-outputs-shared",
@@ -158,7 +170,8 @@
     "Outputs": [
       "o1",
       "o2"
-    ]
+    ],
+    "Description": ""
   },
   {
     "Type": "without-class-without-params",
@@ -167,6 +180,17 @@
     "Outputs": [
       "o1",
       "o2"
-    ]
+    ],
+    "Description": ""
+  },
+  {
+    "Type": "without-class-without-params",
+    "Class": "(any)",
+    "Params": [],
+    "Outputs": [
+      "o1",
+      "o2"
+    ],
+    "Description": "without-class-without-params-with-description"
   }
 ]
diff --git a/internal/command/testdata/provisioners.list.valid.table.golden b/internal/command/testdata/provisioners.list.valid.table.golden
index 687376d..7e8e885 100644
--- a/internal/command/testdata/provisioners.list.valid.table.golden
+++ b/internal/command/testdata/provisioners.list.valid.table.golden
@@ -1,31 +1,33 @@
-+---------------------------------------------------+-------+---------------------+---------+
-|                       TYPE                        | CLASS |       PARAMS        | OUTPUTS |
-+---------------------------------------------------+-------+---------------------+---------+
-| with-class-with-params-in-outputs                 | c1    | p1, po1             | o1, o2  |
-+---------------------------------------------------+-------+---------------------+---------+
-| with-class-with-params-in-shared                  | c1    | p1, psh1            | o1, o2  |
-+---------------------------------------------------+-------+---------------------+---------+
-| with-class-with-params-in-shared-outputs          | c1    | p1, po1, psh1       | o1, o2  |
-+---------------------------------------------------+-------+---------------------+---------+
-| with-class-with-params-in-state                   | c1    | p1, pst1            | o1, o2  |
-+---------------------------------------------------+-------+---------------------+---------+
-| with-class-with-params-in-state-outputs           | c1    | p1, po1, pst1       | o1, o2  |
-+---------------------------------------------------+-------+---------------------+---------+
-| with-class-with-params-in-state-outputs-shared    | c1    | p1, po1, psh1, pst1 | o1, o2  |
-+---------------------------------------------------+-------+---------------------+---------+
-| with-class-without-params                         | c1    |                     | o1, o2  |
-+---------------------------------------------------+-------+---------------------+---------+
-| without-class-with-params-in-outputs              | (any) | p1, po1             | o1, o2  |
-+---------------------------------------------------+-------+---------------------+---------+
-| without-class-with-params-in-shared               | (any) | p1, psh1            | o1, o2  |
-+---------------------------------------------------+-------+---------------------+---------+
-| without-class-with-params-in-shared-outputs       | (any) | p1, po1, psh1       | o1, o2  |
-+---------------------------------------------------+-------+---------------------+---------+
-| without-class-with-params-in-state                | (any) | p1, psh1            | o1, o2  |
-+---------------------------------------------------+-------+---------------------+---------+
-| without-class-with-params-in-state-outputs        | (any) | p1, po1, pst1       | o1, o2  |
-+---------------------------------------------------+-------+---------------------+---------+
-| without-class-with-params-in-state-outputs-shared | (any) | p1, po1, psh1, pst1 | o1, o2  |
-+---------------------------------------------------+-------+---------------------+---------+
-| without-class-without-params                      | (any) |                     | o1, o2  |
-+---------------------------------------------------+-------+---------------------+---------+
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+|                       TYPE                        | CLASS |       PARAMS        | OUTPUTS |                  DESCRIPTION                  |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| with-class-with-params-in-outputs                 | c1    | p1, po1             | o1, o2  |                                               |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| with-class-with-params-in-shared                  | c1    | p1, psh1            | o1, o2  |                                               |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| with-class-with-params-in-shared-outputs          | c1    | p1, po1, psh1       | o1, o2  |                                               |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| with-class-with-params-in-state                   | c1    | p1, pst1            | o1, o2  |                                               |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| with-class-with-params-in-state-outputs           | c1    | p1, po1, pst1       | o1, o2  |                                               |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| with-class-with-params-in-state-outputs-shared    | c1    | p1, po1, psh1, pst1 | o1, o2  |                                               |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| with-class-without-params                         | c1    |                     | o1, o2  |                                               |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| without-class-with-params-in-outputs              | (any) | p1, po1             | o1, o2  |                                               |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| without-class-with-params-in-shared               | (any) | p1, psh1            | o1, o2  |                                               |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| without-class-with-params-in-shared-outputs       | (any) | p1, po1, psh1       | o1, o2  |                                               |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| without-class-with-params-in-state                | (any) | p1, psh1            | o1, o2  |                                               |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| without-class-with-params-in-state-outputs        | (any) | p1, po1, pst1       | o1, o2  |                                               |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| without-class-with-params-in-state-outputs-shared | (any) | p1, po1, psh1, pst1 | o1, o2  |                                               |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| without-class-without-params                      | (any) |                     | o1, o2  |                                               |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
+| without-class-without-params                      | (any) |                     | o1, o2  | without-class-without-params-with-description |
++---------------------------------------------------+-------+---------------------+---------+-----------------------------------------------+
diff --git a/internal/provisioners/templateprov/template_test.go b/internal/provisioners/templateprov/template_test.go
index 546a6dc..e2c2524 100644
--- a/internal/provisioners/templateprov/template_test.go
+++ b/internal/provisioners/templateprov/template_test.go
@@ -33,6 +33,7 @@ func TestProvision(t *testing.T) {
 	p, err := Parse(map[string]interface{}{
 		"uri":              "template://example",
 		"type":             resUid.Type(),
+		"description":      "desc",
 		"expected_outputs": []string{"b", "c"},
 		"supported_params": []string{"ptest"},
 		"init": `
@@ -101,4 +102,5 @@ some-svc:
 	assert.Equal(t, []string{"ptest"}, p.Params())
 	assert.Equal(t, "(any)", p.Class())
 	assert.Equal(t, resUid.Type(), p.Type())
+	assert.Equal(t, "desc", p.Description())
 }