Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Azure Pipeline Integration #153

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package metrik.project.domain.model

enum class PipelineType {
JENKINS, BAMBOO, BAMBOO_DEPLOYMENT, GITHUB_ACTIONS, BUDDY, NOT_SUPPORTED
JENKINS, BAMBOO, BAMBOO_DEPLOYMENT, GITHUB_ACTIONS, AZURE_PIPELINES, BUDDY, NOT_SUPPORTED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package metrik.project.domain.service.azurepipelines

import feign.FeignException
import feign.codec.DecodeException
import metrik.project.domain.model.Execution
import metrik.project.domain.model.PipelineConfiguration
import metrik.project.domain.repository.BuildRepository
import metrik.project.domain.service.PipelineService
import metrik.project.domain.service.githubactions.GithubActionsPipelineService
import metrik.project.exception.PipelineConfigVerifyException
import metrik.project.infrastructure.azure.feign.AzureFeignClient
import metrik.project.rest.vo.response.SyncProgress
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.net.URL

@Service("azurePipelinesPipelineService")
class AzurePipelinesPipelineService(
private val azureFeignClient: AzureFeignClient,
private val buildRepository: BuildRepository,
) : PipelineService {
private var logger = LoggerFactory.getLogger(javaClass.name)
override fun syncBuildsProgressively(
pipeline: PipelineConfiguration,
emitCb: (SyncProgress) -> Unit
): List<Execution> {
TODO("Not yet implemented")
}

override fun verifyPipelineConfiguration(pipeline: PipelineConfiguration) {
logger.info(
"Started verification for Azure Pipelines [name: ${pipeline.name}, url: ${pipeline.url}, " +
"type: ${pipeline.type}]"
)

try {
val (organization, project) = getOrganizationProjectFromUrl(pipeline.url)
azureFeignClient.retrieveMultiplePipelines(pipeline.credential, organization, project)
?: throw PipelineConfigVerifyException("Verify failed")
} catch (ex: FeignException.FeignServerException) {
throw PipelineConfigVerifyException("Verify website unavailable")
} catch (ex: FeignException.FeignClientException) {
throw PipelineConfigVerifyException("Verify failed")
} catch (ex: FeignException) {
throw PipelineConfigVerifyException("Verify failed")
}
}

override fun getStagesSortedByName(pipelineId: String): List<String> {
return buildRepository.getAllBuilds(pipelineId)
.flatMap { it.stages }
.map { it.name }
.distinct()
.sortedBy { it.uppercase() }
.toList()
}

private fun getOrganizationProjectFromUrl(url: String): Pair<String, String> {
val components = URL(url).path.split("/")
val organization = components[components.size - organizationIndex]
val project = components.last()
return Pair(organization, project)
}

private companion object {
const val organizationIndex = 2
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import metrik.project.domain.service.PipelineService
import metrik.project.domain.service.buddy.BuddyPipelineService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import java.nio.channels.Pipe

@Component
class PipelineServiceFactory(
Expand All @@ -13,14 +14,16 @@ class PipelineServiceFactory(
@Autowired private val githubActionsPipelineService: PipelineService,
@Autowired private val bambooDeploymentPipelineService: PipelineService,
@Autowired private val buddyPipelineService: BuddyPipelineService,
@Autowired private val noopPipelineService: PipelineService
@Autowired private val noopPipelineService: PipelineService,
@Autowired private val azurePipelinesPipelineService: PipelineService
) {
fun getService(pipelineType: PipelineType): PipelineService {
return when (pipelineType) {
PipelineType.JENKINS -> this.jenkinsPipelineService
PipelineType.BAMBOO -> this.bambooPipelineService
PipelineType.GITHUB_ACTIONS -> this.githubActionsPipelineService
PipelineType.BAMBOO_DEPLOYMENT -> this.bambooDeploymentPipelineService
PipelineType.AZURE_PIPELINES -> this.azurePipelinesPipelineService
PipelineType.BUDDY -> this.buddyPipelineService
else -> this.noopPipelineService
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package metrik.project.infrastructure.azure.feign

import feign.Headers
import feign.RequestInterceptor
import feign.RequestTemplate
import metrik.project.infrastructure.azure.feign.response.MultiplePipelineResponse
import metrik.project.infrastructure.github.feign.response.BranchResponse
import metrik.project.infrastructure.github.feign.response.CommitResponse
import metrik.project.infrastructure.github.feign.response.MultipleRunResponse
import metrik.project.infrastructure.github.feign.response.SingleRunResponse
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestParam
import java.util.Base64

@FeignClient(
value = "azure-api",
url = "https://dev.azure.com/",
configuration = [AzureFeignClientConfiguration::class]
)
interface AzureFeignClient {
@GetMapping("{organization}/{project}/_apis/pipelines?api-version=6.1-preview.1")
@Headers("Content-Type: application/json")
fun retrieveMultiplePipelines(
@RequestHeader("credential") credential: String,
@PathVariable("organization") organization: String,
@PathVariable("project") project: String,
): MultiplePipelineResponse?
}

class AzureFeignClientConfiguration : RequestInterceptor {
override fun apply(template: RequestTemplate?) {
val pat = template!!.headers()["credential"]!!.first()
val encode = Base64.getEncoder().encodeToString("dora:$pat".encodeToByteArray())
template.header("Authorization", "Basic $encode")
template.removeHeader("credential")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package metrik.project.infrastructure.azure.feign.response

import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.annotation.JsonNaming

@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
data class MultiplePipelineResponse(
val count: Int,
val value: List<Value>
)

data class Value(
val _links: Links,
val folder: String,
val id: Int,
val name: String,
val revision: Int,
val url: String
)

data class Links(
val self: Self,
val web: Web
)

data class Self(
val href: String
)

data class Web(
val href: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package metrik.project.rest.vo.request

import metrik.project.domain.model.PipelineConfiguration
import metrik.project.domain.model.PipelineType
import java.net.URL
import javax.validation.constraints.NotBlank

class AzurePipelinesPipelineRequest(
@field:NotBlank(message = "Name cannot be empty") val name: String,
@field:NotBlank(message = "Credential cannot be empty") val credential: String,
url: String
) : PipelineRequest(url, PipelineType.AZURE_PIPELINES.toString()) {
override fun toPipeline(projectId: String, pipelineId: String) = PipelineConfiguration(
id = pipelineId,
projectId = projectId,
name = name,
username = null,
credential = credential,
url = url,
type = PipelineType.valueOf(type)
)
}

class AzurePipelinesVerificationRequest(
@field:NotBlank(message = "Credential cannot be null or empty") val credential: String,
url: String
) : PipelineVerificationRequest(url, PipelineType.AZURE_PIPELINES.toString()) {
override fun toPipeline() = PipelineConfiguration(
credential = credential,
url = url,
type = PipelineType.valueOf(type)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ data class UpdateProjectRequest(
JsonSubTypes.Type(value = BambooDeploymentPipelineRequest::class, name = "BAMBOO_DEPLOYMENT"),
JsonSubTypes.Type(value = JenkinsPipelineRequest::class, name = "JENKINS"),
JsonSubTypes.Type(value = GithubActionsPipelineRequest::class, name = "GITHUB_ACTIONS"),
JsonSubTypes.Type(value = AzurePipelinesPipelineRequest::class, name = "AZURE_PIPELINES"),
JsonSubTypes.Type(value = BuddyPipelineRequest::class, name = "BUDDY")
)
abstract class PipelineRequest(
@field:NotBlank(message = "URL cannot be empty")
val url: String,
@field:EnumConstraint(
acceptedValues = ["JENKINS", "BAMBOO", "BAMBOO_DEPLOYMENT", "GITHUB_ACTIONS", "BUDDY"],
message = "Allowed types: JENKINS, BAMBOO, BAMBOO_DEPLOYMENT, GITHUB_ACTIONS, BUDDY"
acceptedValues = ["JENKINS", "BAMBOO", "BAMBOO_DEPLOYMENT", "GITHUB_ACTIONS", "AZURE_PIPELINES", "BUDDY"],
message = "Allowed types: JENKINS, BAMBOO, BAMBOO_DEPLOYMENT, GITHUB_ACTIONS, AZURE_PIPELINES, BUDDY"
)
var type: String
) {
Expand All @@ -53,14 +54,15 @@ abstract class PipelineRequest(
JsonSubTypes.Type(value = BambooDeploymentVerificationRequest::class, name = "BAMBOO_DEPLOYMENT"),
JsonSubTypes.Type(value = JenkinsVerificationRequest::class, name = "JENKINS"),
JsonSubTypes.Type(value = GithubActionsVerificationRequest::class, name = "GITHUB_ACTIONS"),
JsonSubTypes.Type(value = AzurePipelinesVerificationRequest::class, name = "AZURE_PIPELINES"),
JsonSubTypes.Type(value = BuddyVerificationRequest::class, name = "BUDDY"),
)
abstract class PipelineVerificationRequest(
@field:NotBlank(message = "URL cannot be empty")
val url: String,
@field:EnumConstraint(
acceptedValues = ["JENKINS", "BAMBOO", "BAMBOO_DEPLOYMENT", "GITHUB_ACTIONS", "BUDDY"],
message = "Allowed types: JENKINS, BAMBOO, BAMBOO_DEPLOYMENT, GITHUB_ACTIONS, BUDDY"
acceptedValues = ["JENKINS", "BAMBOO", "BAMBOO_DEPLOYMENT", "GITHUB_ACTIONS", "AZURE_PIPELINES", "BUDDY"],
message = "Allowed types: JENKINS, BAMBOO, BAMBOO_DEPLOYMENT, GITHUB_ACTIONS, AZURE_PIPELINES, BUDDY"
)
val type: String,
) {
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/clients/pipelineApis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,24 @@ export interface GithubActions extends BasePipeline {
type: PipelineTool.GITHUB_ACTIONS;
}

export interface AzurePipelines extends BasePipeline {
type: PipelineTool.AZURE_PIPELINES;
}

export interface BambooDeployedPipeline extends BasePipeline {
type: PipelineTool.BAMBOO_DEPLOYMENT;
}

export type Pipeline = JenkinsPipeline | BambooPipeline | GithubActions | BambooDeployedPipeline;
export type Pipeline =
| JenkinsPipeline
| BambooPipeline
| GithubActions
| AzurePipelines
| BambooDeployedPipeline;

export type PipelineVerification =
| Omit<JenkinsPipeline, "id" | "name">
| Omit<BambooPipeline, "id" | "name">
| Omit<GithubActions, "id" | "name">
| Omit<AzurePipelines, "id" | "name">
| Omit<BambooDeployedPipeline, "id" | "name">;
6 changes: 6 additions & 0 deletions frontend/src/components/PipelineSetup/PipelineSetup.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AzurePipelines,
BambooDeployedPipeline,
BambooPipeline,
GithubActions,
Expand All @@ -20,12 +21,14 @@ const { Item, useForm } = Form;
type JenkinsFormValues = Omit<JenkinsPipeline, "id">;
type BambooFormValues = Omit<BambooPipeline, "id">;
type GithubActionsFormValues = Omit<GithubActions, "id">;
type AzurePipelinesFormValues = Omit<AzurePipelines, "id">;
type BambooDeployedFormValues = Omit<BambooDeployedPipeline, "id">;

export type FormValues =
| JenkinsFormValues
| BambooFormValues
| GithubActionsFormValues
| AzurePipelinesFormValues
| BambooDeployedFormValues;

const groupTitleStyles = css({ fontWeight: "bold", display: "inline-block", marginBottom: 12 });
Expand Down Expand Up @@ -145,6 +148,9 @@ const PipelineSetup: FC<{
<Option value={PipelineTool.GITHUB_ACTIONS}>
{PipelineTool.GITHUB_ACTIONS}
</Option>
<Option value={PipelineTool.AZURE_PIPELINES}>
{PipelineTool.AZURE_PIPELINES}
</Option>
<Option value={PipelineTool.BUDDY}>{PipelineTool.BUDDY}</Option>
</Select>
</Item>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/models/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export enum PipelineTool {
JENKINS = "JENKINS",
GITHUB_ACTIONS = "GITHUB_ACTIONS",
BAMBOO_DEPLOYMENT = "BAMBOO_DEPLOYMENT",
AZURE_PIPELINES = "AZURE_PIPELINES",
BUDDY = "BUDDY",
}
54 changes: 54 additions & 0 deletions frontend/src/utils/pipelineConfig/azurePipelinesConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from "react";

import { InfoCircleOutlined } from "@ant-design/icons";
import { PipelineConfig } from "./jenkinsConfig";

export const AZURE_PIPELINES_CONFIG: PipelineConfig[] = [
{
gutter: 24,
children: [
{
span: 8,
name: "name",
label: "Project Name",
rules: [
{
required: true,
whitespace: true,
message: "Please input name of your project on Azure Pipeline.",
},
],
},
{
span: 16,
name: "url",
label: "Project URL",
placeholder: "e.g: https://dev.azure.com/organization/project",
tooltip: {
icon: <InfoCircleOutlined />,
title:
'URL of the project. Please ensure the URL is complete and including the organization/project name. e.g. "https://dev.azure.com/{organization}/{project}"',
},
rules: [{ required: true, whitespace: true, message: "Please input the project URL." }],
},
],
},
{
gutter: 24,
children: [
{
span: 8,
name: "credential",
type: "password",
label: "Personal Access Token",
tooltip: {
icon: <InfoCircleOutlined />,
title:
"The PAT (Personal Access Token) will be used to invoke Azure Pipeline APIs to fetch pipeline run status. Tokens can be narrowly scoped to allow only the read access to project and pipeline. Don't know how to manage tokens? Go to: https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate",
},

rules: [{ required: true, whitespace: true, message: "Please input access token." }],
},
],
},
];
Loading