Skip to content

Commit a52622b

Browse files
authored
docs(integrations): list and initial user guide (#197)
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
1 parent d9e11f4 commit a52622b

File tree

7 files changed

+224
-36
lines changed

7 files changed

+224
-36
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ Operators can set up third-party integrations such as [Dependency-Track](https:/
108108

109109
Ops can mix and match with different integrations while **not requiring developers to make any changes on their side**!
110110

111-
You can see the list of available integrations by running `chainloop integration available list` or by browsing [their source code](./app/controlplane/plugins/).
111+
To learn more and to find the list of available integrations, check our [integrations page](./docs/integrations.md)
112112

113113
### Role-tailored experience
114114

app/controlplane/plugins/core/dependency-track/v1/extension.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func New(l log.Logger) (sdk.FanOut, error) {
6363
base, err := sdk.NewFanOut(
6464
&sdk.NewParams{
6565
ID: "dependency-track",
66-
Version: "0.2",
66+
Version: "1.2",
6767
Description: description,
6868
Logger: l,
6969
InputSchema: &sdk.InputSchema{

app/controlplane/plugins/core/discord-webhook/v1/discord.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func New(l log.Logger) (sdk.FanOut, error) {
5757
base, err := sdk.NewFanOut(
5858
&sdk.NewParams{
5959
ID: "discord-webhook",
60-
Version: "0.1",
60+
Version: "1.1",
6161
Description: "Send attestations to Discord",
6262
Logger: l,
6363
InputSchema: &sdk.InputSchema{

app/controlplane/plugins/core/oci-registry/v1/ociregistry.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func New(l log.Logger) (sdk.FanOut, error) {
5959
base, err := sdk.NewFanOut(
6060
&sdk.NewParams{
6161
ID: "oci-registry",
62-
Version: "0.1",
62+
Version: "1.0",
6363
Description: "Send attestations to a compatible OCI registry",
6464
Logger: l,
6565
InputSchema: &sdk.InputSchema{

app/controlplane/plugins/core/smtp/v1/extension.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func New(l log.Logger) (sdk.FanOut, error) {
6262
base, err := sdk.NewFanOut(
6363
&sdk.NewParams{
6464
ID: "smtp",
65-
Version: "0.1",
65+
Version: "1.0",
6666
Description: description,
6767
Logger: l,
6868
InputSchema: &sdk.InputSchema{

app/controlplane/plugins/sdk/readme-generator/main.go

Lines changed: 97 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16-
//go:generate go run main.go --dir ../../core
16+
//go:generate go run main.go --dir ../../core --integrations-index-path ../../../../../docs/integrations.md
1717

1818
package main
1919

@@ -27,17 +27,16 @@ import (
2727
"path/filepath"
2828
"regexp"
2929
"sort"
30+
"strings"
3031

3132
"github.com/chainloop-dev/chainloop/app/controlplane/plugins"
3233
"github.com/chainloop-dev/chainloop/app/controlplane/plugins/sdk/v1"
3334
"github.com/go-kratos/kratos/v2/log"
3435
)
3536

36-
const registrationInputHeader = "## Registration Input Schema"
37-
const attachmentInputHeader = "## Attachment Input Schema"
38-
3937
// base path to the plugins directory
4038
var pluginsDir string
39+
var integrationsIndexPath string
4140

4241
// Enhance README.md files for the registrations with the registration and attachment input schemas
4342
func mainE() error {
@@ -48,47 +47,113 @@ func mainE() error {
4847
return fmt.Errorf("failed to load plugins: %w", err)
4948
}
5049

50+
// Update the list of available plugins
51+
// Update each readme file
5152
for _, e := range plugins {
52-
// Find README file and extract its content
53-
file, err := os.OpenFile(filepath.Join(pluginsDir, e.Describe().ID, "v1", "README.md"), os.O_RDWR, 0644)
54-
if err != nil {
55-
return fmt.Errorf("failed to open README.md file: %w", err)
53+
if err := updatePluginReadme(e); err != nil {
54+
return fmt.Errorf("failed to update README.md file: %w", err)
5655
}
56+
}
5757

58-
fileContent, err := io.ReadAll(file)
59-
if err != nil {
60-
return fmt.Errorf("failed to read README.md file: %w", err)
61-
}
58+
// Update integrations index file
59+
if err := updateIntegrationsIndex(plugins); err != nil {
60+
return fmt.Errorf("failed to update integrations index: %w", err)
61+
}
6262

63-
// Replace/Add registration input schema
64-
fileContent, err = addSchemaToSection(fileContent, registrationInputHeader, e.Describe().RegistrationJSONSchema)
65-
if err != nil {
66-
return fmt.Errorf("failed to add registration schema to README.md file: %w", err)
67-
}
63+
return nil
64+
}
6865

69-
// Replace/Add attachment input schema
70-
fileContent, err = addSchemaToSection(fileContent, attachmentInputHeader, e.Describe().AttachmentJSONSchema)
71-
if err != nil {
72-
return fmt.Errorf("failed to add attachment schema to README.md file: %w", err)
73-
}
66+
func updateIntegrationsIndex(plugins sdk.AvailablePlugins) error {
67+
const indexHeader = "## Available Integrations"
7468

75-
// Write the new content in the file
76-
_, err = file.Seek(0, 0)
77-
if err != nil {
78-
return fmt.Errorf("failed to seek README.md file: %w", err)
79-
}
69+
// Find README integrationsIndex and extract its content
70+
indexFile, err := os.OpenFile(integrationsIndexPath, os.O_RDWR, 0644)
71+
if err != nil {
72+
return fmt.Errorf("failed to open index file %q: %w", integrationsIndexPath, err)
73+
}
74+
defer indexFile.Close()
8075

81-
_, err = file.Write(fileContent)
82-
if err != nil {
83-
return fmt.Errorf("failed to write README.md file: %w", err)
76+
fileContent, err := io.ReadAll(indexFile)
77+
if err != nil {
78+
return fmt.Errorf("failed to read file %q: %w", integrationsIndexPath, err)
79+
}
80+
81+
indexTable := "| ID | Version | Description | Material Requirement |\n| --- | --- | --- | --- |\n"
82+
for _, p := range plugins {
83+
info := p.Describe()
84+
// Load the materials
85+
var subscribedMaterials = make([]string, 0)
86+
for _, m := range info.SubscribedInputs.Materials {
87+
subscribedMaterials = append(subscribedMaterials, m.Type.String())
8488
}
8589

86-
_ = l.Log(log.LevelInfo, "msg", "README.md file updated", "plugin", e.Describe().ID)
90+
// We need to full URL path because we render this file in the website
91+
const repoBase = "https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core"
92+
pathToPlugin := filepath.Join(repoBase, p.Describe().ID, "v1", "README.md")
93+
94+
indexTable += fmt.Sprintf("| [%s](%s) | %s | %s | %s |\n", info.ID, pathToPlugin, info.Version, info.Description, strings.Join(subscribedMaterials, ", "))
95+
}
96+
97+
// Replace the table
98+
section := indexHeader + "\n\n" + indexTable + "\n"
99+
// Find the content that starts with the indexHeader and contains a markdown table
100+
// letters, |, _, -, \n, separators, ... are allowed between the indexHeader and the table
101+
r := regexp.MustCompile(fmt.Sprintf("%s\n*[\\w|\\||\\-|\\s|\\.|_|\\[|\\]|\\(|\\)|\\/|:]*", indexHeader))
102+
103+
fileContent = r.ReplaceAllLiteral(fileContent, []byte(section))
104+
105+
return truncateAndWriteFile(indexFile, fileContent)
106+
}
107+
108+
func truncateAndWriteFile(f *os.File, content []byte) error {
109+
// Write the new content in the file
110+
if err := f.Truncate(0); err != nil {
111+
return fmt.Errorf("failed to truncate file: %w", err)
112+
}
113+
114+
if _, err := f.Seek(0, 0); err != nil {
115+
return fmt.Errorf("failed to seek file: %w", err)
116+
}
117+
118+
_, err := f.Write(content)
119+
if err != nil {
120+
return fmt.Errorf("failed to write file %q: %w", integrationsIndexPath, err)
87121
}
88122

89123
return nil
90124
}
91125

126+
func updatePluginReadme(p sdk.FanOut) error {
127+
const registrationInputHeader = "## Registration Input Schema"
128+
const attachmentInputHeader = "## Attachment Input Schema"
129+
130+
// Find README file and extract its content
131+
file, err := os.OpenFile(filepath.Join(pluginsDir, p.Describe().ID, "v1", "README.md"), os.O_RDWR, 0644)
132+
if err != nil {
133+
return fmt.Errorf("failed to open README.md file: %w", err)
134+
}
135+
defer file.Close()
136+
137+
fileContent, err := io.ReadAll(file)
138+
if err != nil {
139+
return fmt.Errorf("failed to read README.md file: %w", err)
140+
}
141+
142+
// Replace/Add registration input schema
143+
fileContent, err = addSchemaToSection(fileContent, registrationInputHeader, p.Describe().RegistrationJSONSchema)
144+
if err != nil {
145+
return fmt.Errorf("failed to add registration schema to README.md file: %w", err)
146+
}
147+
148+
// Replace/Add attachment input schema
149+
fileContent, err = addSchemaToSection(fileContent, attachmentInputHeader, p.Describe().AttachmentJSONSchema)
150+
if err != nil {
151+
return fmt.Errorf("failed to add attachment schema to README.md file: %w", err)
152+
}
153+
154+
return truncateAndWriteFile(file, fileContent)
155+
}
156+
92157
func main() {
93158
if err := mainE(); err != nil {
94159
panic(err)
@@ -97,6 +162,7 @@ func main() {
97162

98163
func init() {
99164
flag.StringVar(&pluginsDir, "dir", "", "base directory for plugins i.e ./core")
165+
flag.StringVar(&integrationsIndexPath, "integrations-index-path", "", "integrations list markdown file i.e docs/integrations.md")
100166
flag.Parse()
101167
}
102168

docs/integrations.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Chainloop Integrations
2+
3+
Operators can set up third-party integrations that extend Chainloop functionality by operating on your attestation metadata. This logic could go from sending a Slack message, uploading the attestation to a storage backend or sending a Software Bill Of Materials (SBOMs) to a third-party for analysis.
4+
5+
Below you can find the list of currently available integrations. If you can't find the integration you are looking for, feel free [to reach out](https://github.com/chainloop-dev/chainloop/issues) or [contribute your own](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/README.md)!
6+
7+
![FanOut Plugin](./img/fanout.png)
8+
9+
## Available Integrations
10+
11+
| ID | Version | Description | Material Requirement |
12+
| --- | --- | --- | --- |
13+
| [dependency-track](https:/github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/dependency-track/v1/README.md) | 1.2 | Send CycloneDX SBOMs to your Dependency-Track instance | SBOM_CYCLONEDX_JSON |
14+
| [smtp](https:/github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/smtp/v1/README.md) | 1.0 | Send emails with information about a received attestation | |
15+
| [oci-registry](https:/github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/oci-registry/v1/README.md) | 1.0 | Send attestations to a compatible OCI registry | |
16+
| [discord-webhook](https:/github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/discord-webhook/v1/README.md) | 1.1 | Send attestations to Discord | |
17+
18+
## How to use integrations
19+
20+
First you need to make sure that the integration that you are looking for is available in your Chainloop instance, to do so you will
21+
22+
```console
23+
$ chainloop integration available list
24+
┌──────────────────┬─────────┬──────────────────────┬───────────────────────────────────────────────────────────┐
25+
│ ID │ VERSION │ MATERIAL REQUIREMENT │ DESCRIPTION │
26+
├──────────────────┼─────────┼──────────────────────┼───────────────────────────────────────────────────────────┤
27+
│ dependency-track │ 1.2 │ SBOM_CYCLONEDX_JSON │ Send CycloneDX SBOMs to your Dependency-Track instance │
28+
├──────────────────┼─────────┼──────────────────────┼───────────────────────────────────────────────────────────┤
29+
│ smtp │ 1.0 │ │ Send emails with information about a received attestation │
30+
├──────────────────┼─────────┼──────────────────────┼───────────────────────────────────────────────────────────┤
31+
│ oci-registry │ 1.0 │ │ Send attestations to a compatible OCI registry │
32+
├──────────────────┼─────────┼──────────────────────┼───────────────────────────────────────────────────────────┤
33+
│ discord-webhook │ 1.1 │ │ Send attestations to Discord │
34+
└──────────────────┴─────────┴──────────────────────┴───────────────────────────────────────────────────────────┘
35+
```
36+
37+
Once you find the integration you are looking for, i.e `oci-registry`, it's time to configure them.
38+
39+
Configuring an extension has two steps: 1) register the extension in your organization and 2)attach the extension to your workflows.
40+
41+
### Registering an extension
42+
43+
Registration is when a specific instance of the integration is configured on a Chainloop organization. A registered instance is then available to be attached to any workflow.
44+
45+
In our case, we want to register an instance of the `oci-registry` integration. To do so, we need to first figure out what configuration parameters are required by the integration. We can do so by running:
46+
47+
```console
48+
chainloop integration available describe --id oci-registry
49+
┌──────────────┬─────────┬──────────────────────┬────────────────────────────────────────────────┐
50+
│ ID │ VERSION │ MATERIAL REQUIREMENT │ DESCRIPTION │
51+
├──────────────┼─────────┼──────────────────────┼────────────────────────────────────────────────┤
52+
│ oci-registry │ 1.1 │ │ Send attestations to a compatible OCI registry │
53+
└──────────────┴─────────┴──────────────────────┴────────────────────────────────────────────────┘
54+
┌──────────────────────────────────────────────────────────────┐
55+
│ Registration inputs │
56+
├────────────┬────────┬──────────┬─────────────────────────────┤
57+
│ FIELD │ TYPE │ REQUIRED │ DESCRIPTION │
58+
├────────────┼────────┼──────────┼─────────────────────────────┤
59+
│ password │ string │ yes │ OCI repository password │
60+
│ repository │ string │ yes │ OCI repository uri and path │
61+
│ username │ string │ yes │ OCI repository username │
62+
└────────────┴────────┴──────────┴─────────────────────────────┘
63+
┌─────────────────────────────────────────────────────────────────────────┐
64+
│ Attachment inputs │
65+
├────────┬────────┬──────────┬────────────────────────────────────────────┤
66+
│ FIELD │ TYPE │ REQUIRED │ DESCRIPTION │
67+
├────────┼────────┼──────────┼────────────────────────────────────────────┤
68+
│ prefix │ string │ no │ OCI images name prefix (default chainloop) │
69+
└────────┴────────┴──────────┴────────────────────────────────────────────┘
70+
```
71+
72+
In the console output we can see a registration section that indicates that 3 parameters are required, let's go ahead and register it by using our Google Artifact Registry Credentials, for example.
73+
74+
```console
75+
$ chainloop integration registered add oci-registry \
76+
# i.e us-east1-docker.pkg.dev/my-project/chainloop-cas-devel
77+
--opt repository=[region]-docker.pkg.dev/[my-project]/[my-repository] \
78+
--opt username=_json_key \
79+
--opt "password=$(cat service-account.json)"
80+
```
81+
82+
> Note: You can find more examples on how to configure this specific integration [here](https://github.com/chainloop-dev/chainloop/tree/main/app/controlplane/plugins/core/oci-registry/v1)
83+
84+
### Attaching an extension
85+
86+
Once the integration is registered, we can attach it to any workflow. In practice this means that attestations and material information received by this workflow will be sent to the registered integration.
87+
88+
Attachment has at least two required parameters, the workflowID and the registered integration ID. Additionally each integration might have additional configuration parameters that could allow you to customize its behavior. In our case, on the table above, you can see that the `oci-registry` integration has an optional attachment parameter called `prefix` that allows you to customize the name of the image that will be pushed to the registry.
89+
90+
```console
91+
$ chainloop integration attached add --workflow $WID --integration $IID --opt prefix=custom-prefix
92+
```
93+
94+
Congratulations, you are done now! Any attestation or material information received by the workflow will be sent to the registered integration.
95+
96+
## FAQ
97+
98+
### How do I know if an integration is available?
99+
100+
You can use the `chainloop integration available list` command to list all the available integrations.
101+
102+
### How do I know what configuration parameters are required by an integration?
103+
104+
You can use the `chainloop integration available describe` command to list all the required configuration parameters.
105+
106+
### How do I know what registered integrations I have in my organization?
107+
108+
You can use the `chainloop integration registered list` command to list all the registered integrations.
109+
110+
You can also delete a registered integration by using the `chainloop integration registered delete` command.
111+
112+
### How do I know what attachments I have in my organization?
113+
114+
You can use the `chainloop integration attached list` command to list all the attachments, and detach them by using the `chainloop integration attached delete` command.
115+
116+
### What If I can't find the integration I am looking for?
117+
118+
If you can't find the integration you are looking for, feel free [to report it](https://github.com/chainloop-dev/chainloop/issues) or [contribute your own](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/README.md)!
119+
120+
### I am stuck, what do I do?
121+
122+
If you have any questions or run into any issues, don't hesitate to reach out via our [Discord Server](https://discord.gg/f7atkaZact) or open an [Issue](https://github.com/chainloop-dev/chainloop/issues/new). We'll be happy to help.

0 commit comments

Comments
 (0)