Adapter example

This tutorial is a step-by-step instruction on building an example adapter using the CWM SDK. It gives an idea on the adapter structure and on how you provide input to define adapter activities to be consumed by a workflow worker. Before you start, you need to go through the Prerequisites section to set up your development environment.

Important

Running any cwm-sdk command for the first time (except for create-adapter and cwm-sdk version) will trigger the SDK to build a docker image and deploy the adapter builder container. This initial setup may take some time, but once the image is built, subsequent commands will execute much faster.

Step 1: Create a new adapter

In a terminal window, open a command-line terminal and run:

cwm-sdk create-adapter -vendor vendor1 -product product1 -feature feature1

Now you have a new catalog named vendor1.product1 at your home dierctory with the following contents:

Makefile
adapter.properties
docs
go
lib
proto

    ./docs:
        index.html
    ./go:
        common
        go.mod
        feature1

        ./go/common:
            errors.go
            logger.go
        ./go/feature1:

    ./proto:
        vendor1.product1.common.adapter.proto
        vendor1.product1.feature1.adapter.proto

Step 2: Define mock activity

The CWM SDK has generated the .proto files. In the vendor1.product1.feature1.adapter.proto file, define the interface of the adapter:

  1. Open the vendor1.product1.feature1.adapter.proto file with a text editor or inside a terminal window. The contents are as below.

    syntax = "proto3";
    
    package vendor1.product1.feature1;
    
    option go_package = "cisco.com/cwm/adapters/vendor1/product1/feature1";
    
    import "google/protobuf/struct.proto";
    
    service Activities {
    
        // CWM SDK NOTE: Activity functions are defined as RPCs here e.g.
    
        /* Documentation for MyActivity */
        rpc MyActivity(MyRequest) returns (MyResponse);
    }
    
    // CWM SDK NOTE: Messages here e.g.
    
    /* Documentation for MyRequest */
    message MyRequest {
        string                stringInput  = 1;
        int32                 integerInput = 2;
        bool                  booleanInput = 3;
        google.protobuf.Value anyInput     = 4; // CWM SDK NOTE: Useful for accepting a json object from the workflow definition
    }
    
    /* Documentation for MyResponse */
    message MyResponse {
        string                stringOutput  = 1;
        int32                 integerOutput = 2;
        bool                  booleanOutput = 3;
        google.protobuf.Value anyOutput     = 4; // CWM SDK NOTE: Useful for returning a json object to the workflow definition
    }
    
  2. To define your activity, replace the placeholder 'MyActivity' with a mock 'Hello' activity, along with the MyRequest and MyResponse placeholder names and message parameters as shown below:

    service Activities {
        /* Documentation for Hello Activity */
        rpc Hello(MyRequest) returns (MyResponse);
    }
    
    /* Documentation for MyRequest */
    message MyRequest {
        string name = 1;
    }
    
    /* Documentation for MyResponse */
    message MyResponse {
        string message = 1;
    }
    

Step 3: Generate adapter source code

  1. Based on the adapter.proto file that you have edited and on the remaining .proto files, generate the source go code for the adapter and inspect the files. In the main adapter directory, run:
cwm-sdk update-adapter && ls
The output will look like:

    .go/    
        common
        go.mod
        feature1

        go//common:
        errors.go
        logger.go
        vendor1.product1.common.adapter.pb.go

        go//feature1:
        activities.go
        adapter.go
        vendor1.product1.feature1.adapter.pb.go
  1. The .adapter.pb.go files generated using the Protobufs compiler define all the messages from the adapter.proto files.

    caut.gif

    Cautionblank.gifThe .adapter.pb.go files should not be edited manually.

  2. The generated activities.go file contains stubs for all the RPCs you have defined in the .adapter.proto file. Open the file:

    ``` package feature1

    import ( "cisco.com/cwm/adapters/vendor1/product1/common" "context" )

    func (adp Adapter) Hello(ctx context.Context, req MyRequest, cfg common.Config) (MyResponse, error) {

    var res *MyResponse
    var err error
    
    // CWM SDK NOTE: Implement your activity logic here...
    
    return res, err
    

    } ```

  3. Edit the file to return a message:

    func (adp *Adapter) Hello(ctx context.Context, req *MyRequest, cfg *Config) (*MyResponse, error) { return &MyResponse {Message: "Hello, " + req.GetName() + "!"}, nil }

Define another activity

If you wish to add another activity to the existing feature set (go package):

  1. Open and edit the adapter.proto file and define another activity underneath the existing one:

    service Activities {
        rpc Hello(MyRequest) returns (MyResponse);
        rpc Fancy(MyRequest) returns (MyResponse);
    }
    
  2. Update the activities go code using the SDK:

    ``` cwm-sdk extend-adapter -activity fancy -feature feature1

    ```

    After you update the fancy activity part of the .adapter.proto file with a sample logic, update the adapter:

    cwm-sdk update-adapter

    Once the code is generated, the activities.go file is updated with the new 'Fancy' activity stub, while the code for the 'Hello' activity remains.

Step 4: Add another feature

If you wish to add another feature (go package) to the example adapter, use the extend-adapter command. In the main adapter directory, run:

cwm-sdk extend-adapter -feature feature2
  1. A new vendor1.product1.feature2.adapter.proto file has been added for your adapter:

    .proto/
            vendor1.product1.common.adapter.proto
            vendor1.product1.feature2.adapter.proto
            vendor1.product1.feature1.adapter.proto
    
  2. To define activities for the new feature, open the vendor1.product1.feature2.adapter.proto file, and modify the contents accordingly:

    syntax = "proto3";
    
    package vendor1.product1.feature2;
    
    option go_package = "cisco.com/cwm/adapters/vendor1/product1/feature2";
    
    import "google/protobuf/struct.proto";
    
    service Activities {
                /* Documentation for Goodbye Activity */
                rpc Goodbye(MyRequest) returns (MyResponse);
            }
    
            /* Documentation for MyRequest */
            message MyRequest {
                string name = 1;
            }
    
            /* Documentation for MyResponse */
            message MyResponse {
                string message = 1;
            }
    
  3. Generate the code for the 'feature2' package and activities. cwm-sdk update-adapter -features feature2

    .go/goodbyes
        activities.go
        adapter.go
        vendor1.product1.feature2.adapter.pb.go
    

Step 5: Create an installable archive

cwm-sdk create-installable

The generated tar.gz archive contains the all required files of the adapter and can be installed in CWM. The go vendor command has been executed in order to eliminate any external dependencies.

Create custom secrets

This guide describes how to implement a custom secret in a Cisco CWM adapter. Custom secrets allow adapter developers to define and consume provider-specific authentication or sensitive configuration data beyond the built-in secret types (Basic Auth, Bearer Token, and generic Token).

Implement an adapter with custom secret

Create adapter skeleton

Generate a new adapter with custom secret support enabled:

cwm-sdk create-adapter \
  -vendor <vendor> \
  -product <product> \
  -feature <feature> \
  -custom-secret

This command creates the adapter project structure, enables custom secret handling, and generates default .proto and .go files for secrets and resources.

Define custom secret in the .proto

Edit the adapter .proto definition:

proto/<vendor>.<product>.common.adapter.proto

Replace the generic Secret message with your own structured definition, for example:

message Secret {
    message ApiToken {
        string api_key = 1;
        string region  = 2;
    }
    ApiToken apiToken = 1;
} 

Notes:

  • The secret schema is adapter-specific
  • You can define multiple fields and nested messages

Generate go code from the updated .proto

Regenerate adapter code:

cwm-sdk update-adapter

This command generates go structs for the custom secret, updates secret.go and related files, and keeps SDK and adapter code in sync with the proto definition


Implement the secret interface

Edit the following file:

go/common/secret.go

Implement GetCustom() to return the custom secret fields:

func (s *Secret) GetCustom() interface{} {
    return s.GetApiToken()
}

Ensure the built-in secret getters return nil:

func (s *Secret) GetBasicAuth() *secret.BasicAuth { return nil }
func (s *Secret) GetToken() *secret.Token         { return nil }
func (s *Secret) GetBearer() *secret.Bearer       { return nil }

This will ensure that the adapter uses only the custom secret.


Implement authentication logic

Edit:

go/common/resource.go

Update GetAuthentication() to handle your custom secret type, for instance:

func (r *Resource) GetAuthentication() (map[string]string, error) {
    if custom := r.Secret.GetCustom(); custom != nil {
        token := custom.(*common.Secret_ApiToken)
        return map[string]string{
            "Authorization": "ApiKey " + token.ApiKey,
        }, nil
    }
    return nil, nil
}

Best practices:

  • Use logger.Mute(true) when handling secrets, so that secret values are never logged
  • Construct headers, tokens, or request data as required by the target system

Build the adapter installable

Create the adapter installable artifact:

cwm-sdk create-installable

Now the adapter package will include the custom secret definition, and can be deployed and configured in CWM.

Adapter signing

Cisco CWM supports digitally signed adapters to ensure authenticity, integrity, and security compliance. Adapter signing allows CWM to verify that an adapter was produced by a trusted developer and has not been modified after it was built. This feature is backward compatible, so unsigned adapters continue to work.

How adapter signing works

  1. Developer signs the adapter package using a private key,

  2. Signed adapter is uploaded to the CWM platform,

  3. CWM verifies the signature using the developer’s registered public key.

note.gif

Noteblank.gifSignature verification happens automatically during adapter upload and is logged in the API service logs.


How to sign an adapter

Step 1: Generate signing keys

Use OpenSSL to generate an RSA key pair:

openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:4096
openssl pkey -in private.pem -pubout -out public.pem

Key requirements: - Algorithm: RSA
- Format: PKCS#8
- Encoding: PEM
- Key size: Minimum 2048 bits


Step 2: Define the author

Add the author name to adapter.properties. The value must exactly match the registered author name (case-sensitive):

author=developer-name

Step 3: Build and sign the adapter

Create the installable using the private key:

cwm-sdk create-installable -sign-key private.pem

This generates a signed installable file (.tar.gz.signed) that contains: - The original unsigned adapter archive
- A PKCS#1 signature file

Step 4: Register the public key with CWM

Provide the public.pem file and the author name in the CWM UI Workflow Administration → Public Keys.