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

Add Azure DevOps integration #20202

Merged
merged 44 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
a14f259
Add node package
mustard-mh Sep 11, 2024
ae5fd77
first nit changes
mustard-mh Sep 11, 2024
fe82871
nit proto udpate
mustard-mh Sep 12, 2024
11b738f
fixup
mustard-mh Sep 12, 2024
11d18a8
[server] add azure support
mustard-mh Sep 12, 2024
31939b8
fixup
mustard-mh Sep 12, 2024
f22d0a3
fixup
mustard-mh Sep 12, 2024
6ca56e4
server fixup
mustard-mh Sep 12, 2024
fadcb32
[dashboard] changes
mustard-mh Sep 12, 2024
4bcaa90
fixup
mustard-mh Sep 12, 2024
de778be
fixup
mustard-mh Sep 13, 2024
f4b6b74
Fix server bugs
mustard-mh Sep 13, 2024
393ff4e
Fixup
mustard-mh Sep 13, 2024
b33e0ca
Fix dashboard
mustard-mh Sep 13, 2024
262eb92
Fix user integration
mustard-mh Sep 13, 2024
fd55a59
Fix permission update modal
mustard-mh Sep 13, 2024
86450c5
tmp
mustard-mh Sep 13, 2024
7671046
Add unit tests and fix get file content issue
mustard-mh Sep 13, 2024
5abff76
Add readme
mustard-mh Sep 13, 2024
4a1b42a
fix tag and branch parser
mustard-mh Sep 13, 2024
33c9ed6
Update README.md
mustard-mh Sep 13, 2024
80a595a
Remove API tests
mustard-mh Sep 26, 2024
ee1effd
Disable azure devops support for PAYG
mustard-mh Sep 26, 2024
2696492
Revert "Remove API tests"
mustard-mh Sep 26, 2024
6563f4f
Fix tests
mustard-mh Sep 26, 2024
8ed2ce3
Rebase fixup
mustard-mh Sep 26, 2024
c4e75c0
nit fixing
mustard-mh Sep 26, 2024
189c431
revert me
mustard-mh Sep 26, 2024
375d891
Fix integration udpate
mustard-mh Sep 26, 2024
cea6946
Fix ENT-780
mustard-mh Sep 26, 2024
f5467ab
Don't support azure devops on PAYG
mustard-mh Sep 26, 2024
4d5d4f0
dashboard: add comments and remove new Azure DevOps supports on user …
mustard-mh Sep 26, 2024
f33d5a3
Fix push warning and make project a part of owner
mustard-mh Sep 26, 2024
bae8f0a
Proper handle errors
mustard-mh Sep 26, 2024
87afe7f
Fix token can't refresh issue
mustard-mh Sep 27, 2024
8a0e87f
Fix api
mustard-mh Sep 27, 2024
e621899
Add project context supports
mustard-mh Sep 27, 2024
ce12d3d
Update components/server/src/azure-devops/azure-context-parser.spec.ts
mustard-mh Sep 27, 2024
ac749a4
Fix readablestream error
mustard-mh Sep 27, 2024
be43556
Fix clone url
mustard-mh Sep 27, 2024
d5ff0f3
Address feedback
mustard-mh Sep 27, 2024
0314a7d
1
mustard-mh Sep 27, 2024
2ac4ecd
avatar
mustard-mh Sep 27, 2024
d7f086d
Revert "revert me"
mustard-mh Sep 27, 2024
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
14 changes: 14 additions & 0 deletions components/dashboard/src/components/RepositoryFinder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,20 @@ export default function RepositoryFinder({
});
}

if (searchString.length >= 3 && authProviders.data?.some((p) => p.type === AuthProviderType.AZURE_DEVOPS)) {
// ENT-780
result.push({
id: "azure-devops",
element: (
<div className="text-sm text-pk-content-tertiary flex items-center">
<Exclamation2 className="w-4 h-4 mr-2" />
<span>Azure DevOps doesn't support repository searching.</span>
</div>
),
isSelectable: false,
});
}

if (searchString.length < 3) {
// add an element that tells the user to type more
result.push({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2024 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
import { isGitpodIo } from "../../utils";
import { useMemo } from "react";

const optionsForPAYG = [
{ type: AuthProviderType.GITHUB, label: "GitHub" },
{ type: AuthProviderType.GITLAB, label: "GitLab" },
{ type: AuthProviderType.BITBUCKET_SERVER, label: "Bitbucket Server" },
{ type: AuthProviderType.BITBUCKET, label: "Bitbucket Cloud" },
];

const optionsForEnterprise = [...optionsForPAYG, { type: AuthProviderType.AZURE_DEVOPS, label: "Azure DevOps" }];

export const isSupportAzureDevOpsIntegration = () => {
return isGitpodIo();
};

export const useAuthProviderOptionsQuery = (isOrgLevel: boolean) => {
return useMemo(() => {
const isPAYG = isGitpodIo();
// Azure DevOps is not supported for PAYG users and is only available for org-level integrations
// because auth flow is identified by auth provider's host, which will always be `dev.azure.com`
//
// Don't remove this until we can setup an generial application for Azure DevOps (investigate needed)
if (isPAYG || !isOrgLevel) {
return optionsForPAYG;
}
return optionsForEnterprise;
}, [isOrgLevel]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ type CreateAuthProviderArgs = {
clientId: string;
clientSecret: string;
orgId: string;
authorizationUrl?: string;
tokenUrl?: string;
};
};
export const useCreateOrgAuthProviderMutation = () => {
Expand All @@ -28,6 +30,8 @@ export const useCreateOrgAuthProviderMutation = () => {
oauth2Config: {
clientId: provider.clientId,
clientSecret: provider.clientSecret,
authorizationUrl: provider.authorizationUrl,
tokenUrl: provider.tokenUrl,
},
type: provider.type,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ type CreateAuthProviderArgs = {
clientId: string;
clientSecret: string;
userId: string;
authorizationUrl?: string;
tokenUrl?: string;
mustard-mh marked this conversation as resolved.
Show resolved Hide resolved
};
};
export const useCreateUserAuthProviderMutation = () => {
Expand All @@ -28,6 +30,8 @@ export const useCreateUserAuthProviderMutation = () => {
oauth2Config: {
clientId: provider.clientId,
clientSecret: provider.clientSecret,
authorizationUrl: provider.authorizationUrl,
tokenUrl: provider.tokenUrl,
},
type: provider.type,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ type UpdateAuthProviderArgs = {
id: string;
clientId: string;
clientSecret: string;
authorizationUrl?: string;
tokenUrl?: string;
};
};
export const useUpdateOrgAuthProviderMutation = () => {
Expand All @@ -26,6 +28,8 @@ export const useUpdateOrgAuthProviderMutation = () => {
authProviderId: provider.id,
clientId: provider.clientId,
clientSecret: provider.clientSecret,
authorizationUrl: provider.authorizationUrl,
tokenUrl: provider.tokenUrl,
}),
);
return response.authProvider!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ type UpdateAuthProviderArgs = {
id: string;
clientId: string;
clientSecret: string;
authorizationUrl?: string;
tokenUrl?: string;
};
};
export const useUpdateUserAuthProviderMutation = () => {
Expand All @@ -26,6 +28,8 @@ export const useUpdateUserAuthProviderMutation = () => {
authProviderId: provider.id,
clientId: provider.clientId,
clientSecret: provider.clientSecret,
authorizationUrl: provider.authorizationUrl,
tokenUrl: provider.tokenUrl,
}),
);
return response.authProvider!;
Expand Down
1 change: 1 addition & 0 deletions components/dashboard/src/images/azuredevops.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions components/dashboard/src/provider-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_
import bitbucket from "./images/bitbucket.svg";
import github from "./images/github.svg";
import gitlab from "./images/gitlab.svg";
import azuredevops from "./images/azuredevops.svg";
import { gitpodHostUrl } from "./service/service";

function iconForAuthProvider(type: string | AuthProviderType) {
Expand All @@ -24,6 +25,9 @@ function iconForAuthProvider(type: string | AuthProviderType) {
case "BitbucketServer":
case AuthProviderType.BITBUCKET_SERVER:
return <img className="fill-current filter-grayscale w-5 h-5 ml-3 mr-3 my-auto" src={bitbucket} alt="" />;
case "AzureDevOps":
case AuthProviderType.AZURE_DEVOPS:
return <img className="fill-current filter-grayscale w-5 h-5 ml-3 mr-3 my-auto" src={azuredevops} alt="" />;
default:
return <></>;
}
Expand All @@ -39,6 +43,8 @@ export function toAuthProviderLabel(type: AuthProviderType) {
return "Bitbucket Cloud";
case AuthProviderType.BITBUCKET_SERVER:
return "Bitbucket Server";
case AuthProviderType.AZURE_DEVOPS:
return "Azure DevOps";
default:
return "-";
}
Expand All @@ -52,6 +58,8 @@ function simplifyProviderName(host: string) {
return "GitLab";
case "bitbucket.org":
return "Bitbucket";
case "dev.azure.com":
return "Azure DevOps";
default:
return host;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import { useCreateOrgAuthProviderMutation } from "../../data/auth-providers/crea
import { useUpdateOrgAuthProviderMutation } from "../../data/auth-providers/update-org-auth-provider-mutation";
import { authProviderClient, userClient } from "../../service/public-api";
import { LoadingButton } from "@podkit/buttons/LoadingButton";
import {
isSupportAzureDevOpsIntegration,
useAuthProviderOptionsQuery,
} from "../../data/auth-providers/auth-provider-options-query";

type Props = {
provider?: AuthProvider;
Expand All @@ -37,6 +41,10 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
const [host, setHost] = useState<string>(props.provider?.host ?? "");
const [clientId, setClientId] = useState<string>(props.provider?.oauth2Config?.clientId ?? "");
const [clientSecret, setClientSecret] = useState<string>(props.provider?.oauth2Config?.clientSecret ?? "");
const [authorizationUrl, setAuthorizationUrl] = useState(props.provider?.oauth2Config?.authorizationUrl ?? "");
const [tokenUrl, setTokenUrl] = useState(props.provider?.oauth2Config?.tokenUrl ?? "");
const availableProviderOptions = useAuthProviderOptionsQuery(true);
const supportAzureDevOps = isSupportAzureDevOpsIntegration();

const [savedProvider, setSavedProvider] = useState(props.provider);
const isNew = !savedProvider;
Expand Down Expand Up @@ -82,6 +90,21 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
clientSecret.trim().length > 0,
);

const {
message: authorizationUrlError,
onBlur: authorizationUrlOnBlur,
isValid: authorizationUrlValid,
} = useOnBlurError(
`Authorization URL is missing.`,
type !== AuthProviderType.AZURE_DEVOPS || authorizationUrl.trim().length > 0,
);

const {
message: tokenUrlError,
onBlur: tokenUrlOnBlur,
isValid: tokenUrlValid,
} = useOnBlurError(`Token URL is missing.`, type !== AuthProviderType.AZURE_DEVOPS || tokenUrl.trim().length > 0);

// Call our error onBlur handler, and remove prefixed "https://"
const hostOnBlur = useCallback(() => {
hostOnBlurErrorTracking();
Expand Down Expand Up @@ -112,6 +135,8 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {

const trimmedId = clientId.trim();
const trimmedSecret = clientSecret.trim();
const trimmedAuthorizationUrl = authorizationUrl.trim();
const trimmedTokenUrl = tokenUrl.trim();

try {
let newProvider: AuthProvider;
Expand All @@ -123,6 +148,8 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
orgId: team.id,
clientId: trimmedId,
clientSecret: trimmedSecret,
authorizationUrl: trimmedAuthorizationUrl,
tokenUrl: trimmedTokenUrl,
},
});
} else {
Expand All @@ -131,6 +158,8 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
id: savedProvider.id,
clientId: trimmedId,
clientSecret: clientSecret === "redacted" ? "" : trimmedSecret,
authorizationUrl: trimmedAuthorizationUrl,
tokenUrl: trimmedTokenUrl,
},
});
}
Expand Down Expand Up @@ -181,6 +210,8 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
}, [
clientId,
clientSecret,
authorizationUrl,
tokenUrl,
host,
invalidateOrgAuthProviders,
isNew,
Expand All @@ -196,8 +227,8 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
]);

const isValid = useMemo(
() => clientIdValid && clientSecretValid && hostValid,
[clientIdValid, clientSecretValid, hostValid],
() => clientIdValid && clientSecretValid && hostValid && authorizationUrlValid && tokenUrlValid,
[clientIdValid, clientSecretValid, hostValid, authorizationUrlValid, tokenUrlValid],
);

const getNumber = (paramValue: string | null) => {
Expand All @@ -223,7 +254,8 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
<ModalBody>
{isNew && (
<Subheading>
Configure a Git Integration with a self-managed instance of GitLab, GitHub, or Bitbucket Server.
Configure a Git Integration with a self-managed instance of GitLab, GitHub{" "}
{supportAzureDevOps ? ", Bitbucket Server or Azure DevOps" : "or Bitbucket"}.
</Subheading>
)}

Expand All @@ -235,10 +267,11 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
topMargin={false}
onChange={(val) => setType(getNumber(val))}
>
<option value={AuthProviderType.GITHUB}>GitHub</option>
<option value={AuthProviderType.GITLAB}>GitLab</option>
<option value={AuthProviderType.BITBUCKET}>Bitbucket Cloud</option>
<option value={AuthProviderType.BITBUCKET_SERVER}>Bitbucket Server</option>
{availableProviderOptions.map((option) => (
<option key={option.type} value={option.type}>
{option.label}
</option>
))}
</SelectInputField>
<TextInputField
label="Provider Host Name"
Expand All @@ -254,6 +287,25 @@ export const GitIntegrationModal: FunctionComponent<Props> = (props) => {
<InputWithCopy value={redirectURL} tip="Copy the redirect URI to clipboard" />
</InputField>

{type === AuthProviderType.AZURE_DEVOPS && (
<>
<TextInputField
label="Authorization URL"
value={authorizationUrl}
error={authorizationUrlError}
onBlur={authorizationUrlOnBlur}
onChange={setAuthorizationUrl}
/>
<TextInputField
label="Token URL"
value={tokenUrl}
error={tokenUrlError}
onBlur={tokenUrlOnBlur}
onChange={setTokenUrl}
/>
</>
)}

<TextInputField
label={type === AuthProviderType.GITLAB ? "Application ID" : "Client ID"}
value={clientId}
Expand Down Expand Up @@ -314,6 +366,8 @@ const getPlaceholderForIntegrationType = (type: AuthProviderType) => {
return "bitbucket.org";
case AuthProviderType.BITBUCKET_SERVER:
return "bitbucket.example.com";
case AuthProviderType.AZURE_DEVOPS:
return "dev.azure.com";
default:
return "";
}
Expand All @@ -337,6 +391,9 @@ const RedirectUrlDescription: FunctionComponent<RedirectUrlDescriptionProps> = (
case AuthProviderType.BITBUCKET_SERVER:
docsUrl = "https://www.gitpod.io/docs/configure/authentication/bitbucket-server";
break;
case AuthProviderType.AZURE_DEVOPS:
docsUrl = "https://www.gitpod.io/docs/configure/authentication/azure-devops";
mustard-mh marked this conversation as resolved.
Show resolved Hide resolved
break;
default:
return null;
}
Expand Down
3 changes: 2 additions & 1 deletion components/dashboard/src/user-settings/AuthEntryItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ContextMenuEntry } from "../components/ContextMenu";
import { Item, ItemFieldIcon, ItemField, ItemFieldContextMenu } from "../components/ItemsList";
import { AuthProviderDescription } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
import { toAuthProviderLabel } from "../provider-utils";
import { getScopeNameForScope } from "@gitpod/public-api-common/lib/auth-providers";

interface AuthEntryItemParams {
ap: AuthProviderDescription;
Expand Down Expand Up @@ -53,7 +54,7 @@ export const AuthEntryItem = (props: AuthEntryItemParams) => {
</ItemField>
<ItemField className="hidden xl:w-1/3 xl:flex xl:flex-col my-auto">
<span className="my-auto truncate text-gray-500 overflow-ellipsis dark:text-gray-400">
{props.getPermissions(props.ap.id)?.join(", ") || "–"}
{props.getPermissions(props.ap.id)?.map(getScopeNameForScope)?.join(", ") || "–"}
</span>
<span className="text-sm my-auto text-gray-400 dark:text-gray-500">Permissions</span>
</ItemField>
Expand Down
Loading
Loading