Skip to content

Instantly share code, notes, and snippets.

@kirederik
Last active January 29, 2024 15:14
Show Gist options
  • Save kirederik/1629a59facc8d1be4ebce7ed2e5198e1 to your computer and use it in GitHub Desktop.
Save kirederik/1629a59facc8d1be4ebce7ed2e5198e1 to your computer and use it in GitHub Desktop.
Pushing Resource Requests to Bitbucket

Steps

  1. At the root of your backstage directory, run:
    yarn --cwd packages/backend add @backstage/plugin-scaffolder-node
    yarn --cwd packages/backend add zod
    yarn --cwd packages/backend add fs-extra @types/fs-extra
    yarn install
    
  2. Create a custom.ts file in packages/backend/src/plugins/actions/ with the contents of the custom.ts file below
  3. Update your packages/backend/src/plugins/scaffolder.ts with the custom actions. It will look like the scaffolder.ts file below
  4. The custom action uses an appPassword to communicate with the bitbucket repository. To create one, follow the bitbucket documentation
  5. Configure the Bitbucket Cloud integration
  6. In your app-config.yaml, configure the the default author: this is the one the action will use when pushing to the repository

Now your backstage is fully configured and able to push to bitbucket 🎉 Time to configure your Promise!

Your Template must contain at least there three steps, in order:

  • the bitbucket:clone step will clone the repository to the backstage workspace
    • This is the repository where you will push the resource requests
    • You should have flux running on your Platform Cluster listening to changes in this repository
  • the create:file step will create the resource request yaml in the right location
    • it is important to make this unique, so I recommend adding the promise-name, the resource name and the namespace in the filepath somewhere
  • the bitbucket:push step will push to the repository
    • as flux is runnning in your Platform cluster and listening to new documents, it should automatically pick it up and start deploying

There's an example template below for your reference

import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { InputError } from '@backstage/errors';
import { z } from 'zod';
import { Git } from '@backstage/backend-common';
import { Config } from '@backstage/config';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { parseRepoUrl } from '@backstage/plugin-scaffolder-node';
import fs from 'fs-extra';
export const bitbucketClone = (options: {
integrations: ScmIntegrationRegistry;
config: Config;
}) => {
const { integrations, config } = options;
return createTemplateAction({
id: 'bitbucket:clone',
schema: {
input: z.object({
branch: z.string().optional().default('main'),
targetPath: z.string().optional().default('./repository'),
repoUrl: z.string().optional().describe('The remote URL of the file'),
repo: z.string().optional().default(''),
host: z.string().optional().default('bitbucket.org'),
workspace: z.string().optional().default(''),
}),
},
async handler(ctx) {
const {
repoUrl,
branch = 'main',
targetPath = './repostiory',
} = ctx.input;
const { repo, host = 'bitbucket.org', workspace } = repoUrl
? parseRepoUrl(repoUrl, integrations)
: ctx.input;
if (!workspace || !repo) {
throw new InputError(
`Repository location not configured, please check your input. Either repoUrl or [workspace and repo] must be provided`,
);
}
const integrationConfig = integrations.bitbucketCloud.byHost(host);
if (!integrationConfig) {
throw new InputError(
`No matching integration configuration for host ${host}, please check your integrations config`,
);
}
const { username, appPassword } = integrationConfig.config;
if (!username || !appPassword) {
throw new InputError(
`No username or appPassword found for host ${host}, please check your integrations config`,
);
}
const remoteUrl = `https://${host}/${workspace}/${repo}`;
await Git.fromAuth({
username,
password: appPassword,
logger: ctx.logger,
}).clone({
url: remoteUrl,
dir: `${ctx.workspacePath}/${targetPath}`,
ref: branch,
});
},
});
};
export const bitbucketPush = (options: {
integrations: ScmIntegrationRegistry;
config: Config;
}) => {
const { integrations, config } = options;
return createTemplateAction({
id: 'bitbucket:push',
schema: {
input: z.object({
dir: z.string(),
host: z.string().optional().default('bitbucket.org'),
gitAuthorName: z.string().optional(),
gitAuthorEmail: z.string().optional(),
}),
output: z.object({
commitsha: z.string().nullable(),
}),
},
async handler(ctx) {
const {
dir,
host = 'bitbucket.org',
gitAuthorEmail,
gitAuthorName,
} = ctx.input;
if (!host) {
throw new InputError(`host not configured, please check your input`);
}
const integrationConfig = integrations.bitbucketCloud.byHost(host);
if (!integrationConfig) {
throw new InputError(
`No matching integration configuration for host ${host}, please check your integrations config`,
);
}
const { username, appPassword } = integrationConfig.config;
if (!username || !appPassword) {
throw new InputError(
`No username or appPassword found for host ${host}, please check your integrations config`,
);
}
const git = Git.fromAuth({
username,
password: appPassword,
logger: ctx.logger,
});
await git.add({
dir: `${ctx.workspacePath}/${dir}`,
filepath: '.',
});
const gitAuthorInfo = {
name: gitAuthorName
? gitAuthorName
: config.getOptionalString('scaffolder.defaultAuthor.name'),
email: gitAuthorEmail
? gitAuthorEmail
: config.getOptionalString('scaffolder.defaultAuthor.email'),
};
const authorInfo = {
name: gitAuthorInfo?.name ?? 'Scaffolder',
email: gitAuthorInfo?.email ?? 'scaffolder@backstage.io',
};
const commitHash = await git.commit({
dir: `${ctx.workspacePath}/${dir}`,
message: 'New commit from Scaffolder',
author: authorInfo,
committer: authorInfo,
});
await git.push({
dir: `${ctx.workspacePath}/${dir}`,
remote: 'origin',
});
ctx.output('commitsha', commitHash);
},
});
};
export const createFile = () => {
return createTemplateAction({
id: 'create:file',
schema: {
input: z.object({
contents: z.string().describe('The contents of the file'),
filename: z
.string()
.describe('The filename of the file that will be created'),
}),
},
async handler(ctx) {
const { filename, contents } = ctx.input;
await fs.outputFile(`${ctx.workspacePath}/${filename}`, contents);
},
});
};
apiVersion: scaffolder.backstage.io/v1beta3
# https://backstage.io/docs/features/software-catalog/descriptor-format#kind-template
kind: Template
metadata:
name: example-bitbucket-flow
title: Resource Request to BitBucket
description: An example template for creating a resource request in BitBucket
spec:
owner: user:guest
type: service
parameters:
- title: Fill in some steps
required:
- name
properties:
name:
title: Name
type: string
description: Name for the request
ui:autofocus: true
ui:options:
rows: 5
namespace:
title: Namespace
type: string
description: Namespace for the request
ui:autofocus: true
ui:options:
rows: 5
steps:
- id: git-clone
name: Clone Repository
action: bitbucket:clone
input:
targetPath: destinations # optional, defaults to "repository". Is used in the next steps
repo: <repository name>
workspace: <workspace name>
- id: create-manifest-file
name: Create Resource Request
action: create:file
input:
# replace my-promise with your promise name
filename: ./destinations/platform/requests/my-promise/${{ parameters.namespace }}/${{ parameters.name }}/manifest.yaml
# replace the contents with your manifest
contents: |
apiVersion: example.kratix.io/v1alpha1
kind: MyKind
metadata:
name: ${{ parameters.name }}
namespace: ${{ parameters.namespace }}
spec:
text: "Hello World"
- id: git-push
name: Push Changes
action: bitbucket:push
input:
dir: ./destinations
import { CatalogClient } from '@backstage/catalog-client';
import {
createBuiltinActions,
createRouter,
} from '@backstage/plugin-scaffolder-backend';
import { Router } from 'express';
import type { PluginEnvironment } from '../types';
import { ScmIntegrations } from '@backstage/integration';
import { bitbucketClone, bitbucketPush, createFile } from './actions/custom';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const catalogClient = new CatalogClient({
discoveryApi: env.discovery,
});
const integrations = ScmIntegrations.fromConfig(env.config);
const builtInActions = createBuiltinActions({
catalogClient,
integrations,
config: env.config,
reader: env.reader,
});
const actions = [
bitbucketClone({ integrations, config: env.config }),
bitbucketPush({ integrations, config: env.config }),
createFile(),
...builtInActions,
];
return await createRouter({
logger: env.logger,
config: env.config,
database: env.database,
reader: env.reader,
catalogClient,
identity: env.identity,
permissions: env.permissions,
actions,
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment