Skip to content

Commit 11847bb

Browse files
committed
Rewrote repository handler
1 parent 5f1c214 commit 11847bb

File tree

4 files changed

+274
-4
lines changed

4 files changed

+274
-4
lines changed

examples/workspace/repo.basic.bicep

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@ targetScope = 'local'
55

66
param workspaceUrl string
77

8-
98
extension databricksExtension with {
109
workspaceUrl: workspaceUrl
1110
}
1211

13-
resource gitRepo 'Repo' = {
12+
resource gitRepo 'Repository' = {
1413
provider: 'gitHub'
1514
url: 'https://github.com/Gijsreyn/bicep-ext-databricks'
1615
branch: 'main'
1716
}
1817

19-
output repoId string = gitRepo.repoId
18+
output repoId string = gitRepo.id
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
using System.Text.Json;
2+
using Microsoft.Extensions.Logging;
3+
using Databricks.Models;
4+
5+
namespace Databricks.Handlers;
6+
7+
public class DatabricksRepositoryHandler : DatabricksResourceHandlerBase<Repository, RepositoryIdentifiers>
8+
{
9+
private const string ReposApiEndpoint = "2.0/repos";
10+
11+
public DatabricksRepositoryHandler(ILogger<DatabricksRepositoryHandler> logger) : base(logger) { }
12+
13+
protected override async Task<ResourceResponse> Preview(ResourceRequest request, CancellationToken cancellationToken)
14+
{
15+
var existing = await GetRepositoryAsync(request.Config, request.Properties, cancellationToken);
16+
if (existing is not null)
17+
{
18+
request.Properties.Id = existing.id;
19+
request.Properties.HeadCommitId = existing.head_commit_id;
20+
request.Properties.Branch = existing.branch;
21+
if (existing.sparse_checkout?.patterns != null)
22+
{
23+
request.Properties.SparseCheckout = new SparseCheckout
24+
{
25+
Patterns = existing.sparse_checkout.patterns
26+
};
27+
}
28+
}
29+
return GetResponse(request);
30+
}
31+
32+
protected override async Task<ResourceResponse> CreateOrUpdate(ResourceRequest request, CancellationToken cancellationToken)
33+
{
34+
var props = request.Properties;
35+
36+
ValidateRepositoryProperties(props);
37+
38+
_logger.LogInformation("Ensuring repository for provider {Provider} url {Url}", props.Provider, props.Url);
39+
40+
var existing = await GetRepositoryAsync(request.Config, props, cancellationToken);
41+
42+
if (existing is null)
43+
{
44+
_logger.LogInformation("Creating new repository (provider {Provider} url {Url})", props.Provider, props.Url);
45+
var createdRepoId = await CreateRepositoryAsync(request.Config, props, cancellationToken);
46+
existing = await GetRepositoryByIdAsync(request.Config, createdRepoId, cancellationToken)
47+
?? throw new InvalidOperationException("Repository creation did not return repository.");
48+
}
49+
else
50+
{
51+
_logger.LogInformation("Updating existing repository {Id}", (string)existing.id);
52+
await UpdateRepositoryAsync(request.Config, props, existing, cancellationToken);
53+
existing = await GetRepositoryByIdAsync(request.Config, (string)existing.id, cancellationToken)
54+
?? throw new InvalidOperationException("Repository update did not return repository.");
55+
}
56+
57+
props.Id = existing.id;
58+
props.HeadCommitId = existing.head_commit_id;
59+
props.Branch = existing.branch;
60+
if (existing.sparse_checkout?.patterns != null)
61+
{
62+
props.SparseCheckout = new SparseCheckout
63+
{
64+
Patterns = existing.sparse_checkout.patterns
65+
};
66+
}
67+
68+
return GetResponse(request);
69+
}
70+
71+
protected override RepositoryIdentifiers GetIdentifiers(Repository properties) => new()
72+
{
73+
Provider = properties.Provider,
74+
Url = properties.Url,
75+
Path = properties.Path
76+
};
77+
78+
private async Task<dynamic?> GetRepositoryAsync(Configuration configuration, Repository props, CancellationToken ct)
79+
{
80+
try
81+
{
82+
var response = await CallDatabricksApiForResponse<JsonElement>(configuration.WorkspaceUrl, HttpMethod.Get, ReposApiEndpoint, ct);
83+
84+
if (!response.TryGetProperty("repos", out var reposArray))
85+
return null;
86+
87+
foreach (var repo in reposArray.EnumerateArray())
88+
{
89+
var provider = repo.TryGetProperty("provider", out var providerProp) ? providerProp.GetString() : null;
90+
var url = repo.TryGetProperty("url", out var urlProp) ? urlProp.GetString() : null;
91+
var path = repo.TryGetProperty("path", out var pathProp) ? pathProp.GetString() : null;
92+
93+
if (provider == props.Provider.ToString() && url == props.Url &&
94+
(string.IsNullOrEmpty(props.Path) || path == props.Path))
95+
{
96+
return CreateRepositoryObject(repo);
97+
}
98+
}
99+
return null;
100+
}
101+
catch
102+
{
103+
return null;
104+
}
105+
}
106+
107+
private async Task<dynamic?> GetRepositoryByIdAsync(Configuration configuration, string repoId, CancellationToken ct)
108+
{
109+
try
110+
{
111+
var response = await CallDatabricksApiForResponse<JsonElement>(configuration.WorkspaceUrl, HttpMethod.Get, $"{ReposApiEndpoint}/{repoId}", ct);
112+
113+
if (response.ValueKind == JsonValueKind.Undefined || response.ValueKind == JsonValueKind.Null)
114+
return null;
115+
116+
return CreateRepositoryObject(response);
117+
}
118+
catch
119+
{
120+
return null;
121+
}
122+
}
123+
124+
private static dynamic CreateRepositoryObject(JsonElement repo)
125+
{
126+
// Extract sparse checkout patterns if they exist
127+
string[]? patterns = null;
128+
if (repo.TryGetProperty("sparse_checkout", out var sparseCheckout) &&
129+
sparseCheckout.TryGetProperty("patterns", out var patternsArray))
130+
{
131+
patterns = patternsArray.EnumerateArray()
132+
.Select(p => p.GetString())
133+
.Where(s => !string.IsNullOrEmpty(s))
134+
.ToArray()!;
135+
}
136+
137+
return new
138+
{
139+
id = repo.GetProperty("id").GetInt64().ToString(),
140+
provider = repo.TryGetProperty("provider", out var provider) ? provider.GetString() : null,
141+
url = repo.TryGetProperty("url", out var url) ? url.GetString() : null,
142+
path = repo.TryGetProperty("path", out var path) ? path.GetString() : null,
143+
branch = repo.TryGetProperty("branch", out var b) ? b.GetString() : null,
144+
head_commit_id = repo.TryGetProperty("head_commit_id", out var hci) ? hci.GetString() : null,
145+
sparse_checkout = patterns != null ? new { patterns } : null
146+
};
147+
}
148+
149+
private async Task<string> CreateRepositoryAsync(Configuration configuration, Repository props, CancellationToken ct)
150+
{
151+
var createPayload = new Dictionary<string, object?>
152+
{
153+
["provider"] = props.Provider.ToString(),
154+
["url"] = props.Url
155+
};
156+
157+
if (!string.IsNullOrWhiteSpace(props.Path))
158+
createPayload["path"] = props.Path;
159+
160+
if (!string.IsNullOrWhiteSpace(props.Branch))
161+
createPayload["branch"] = props.Branch;
162+
163+
if (props.SparseCheckout?.Patterns != null && props.SparseCheckout.Patterns.Length > 0)
164+
{
165+
createPayload["sparse_checkout"] = new
166+
{
167+
patterns = props.SparseCheckout.Patterns
168+
};
169+
}
170+
171+
var response = await CallDatabricksApiForResponse<JsonElement>(configuration.WorkspaceUrl, HttpMethod.Post, ReposApiEndpoint, ct, createPayload);
172+
if (response.ValueKind == JsonValueKind.Undefined || response.ValueKind == JsonValueKind.Null)
173+
{
174+
throw new InvalidOperationException($"Failed to create repository for provider '{props.Provider}' and url '{props.Url}'.");
175+
}
176+
177+
// Extract the repository ID from the response
178+
if (response.TryGetProperty("id", out var idProp))
179+
{
180+
return idProp.GetInt64().ToString();
181+
}
182+
183+
throw new InvalidOperationException("Repository creation response did not contain an ID.");
184+
}
185+
186+
private async Task UpdateRepositoryAsync(Configuration configuration, Repository props, dynamic existing, CancellationToken ct)
187+
{
188+
var updatePayload = new Dictionary<string, object?>
189+
{
190+
["branch"] = props.Branch
191+
};
192+
193+
if (props.SparseCheckout?.Patterns != null && props.SparseCheckout.Patterns.Length > 0)
194+
{
195+
updatePayload["sparse_checkout"] = new
196+
{
197+
patterns = props.SparseCheckout.Patterns
198+
};
199+
}
200+
201+
var repoId = (string)existing.id;
202+
var response = await CallDatabricksApiForResponse<JsonElement>(configuration.WorkspaceUrl, HttpMethod.Patch, $"{ReposApiEndpoint}/{repoId}", ct, updatePayload);
203+
if (response.ValueKind == JsonValueKind.Undefined || response.ValueKind == JsonValueKind.Null)
204+
{
205+
throw new InvalidOperationException($"Failed to update repository {repoId} for provider '{props.Provider}' and url '{props.Url}'.");
206+
}
207+
}
208+
209+
private static void ValidateRepositoryProperties(Repository props)
210+
{
211+
if (!Uri.TryCreate(props.Url, UriKind.Absolute, out var uri) ||
212+
(uri.Scheme != "https" && uri.Scheme != "http"))
213+
{
214+
throw new ArgumentException("URL must be a valid HTTP or HTTPS URL.", nameof(props.Url));
215+
}
216+
}
217+
}

src/Models/Repository.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Text.Json.Serialization;
2+
using Azure.Bicep.Types.Concrete;
3+
using Bicep.Local.Extension.Types.Attributes;
4+
5+
namespace Databricks.Models;
6+
7+
public enum RepoProvider
8+
{
9+
gitHub,
10+
bitbucketCloud,
11+
gitLab,
12+
azureDevOpsServices,
13+
gitHubEnterprise,
14+
bitbucketServer,
15+
gitLabEnterpriseEdition,
16+
awsCodeCommit
17+
}
18+
19+
public class SparseCheckout
20+
{
21+
[TypeProperty("List of patterns for sparse checkout.")]
22+
public string[]? Patterns { get; set; }
23+
}
24+
25+
public class RepositoryIdentifiers
26+
{
27+
[TypeProperty("The Git provider for the repository.", ObjectTypePropertyFlags.Identifier | ObjectTypePropertyFlags.Required)]
28+
[JsonConverter(typeof(JsonStringEnumConverter))]
29+
public required RepoProvider? Provider { get; set; }
30+
31+
[TypeProperty("The URL of the Git repository.", ObjectTypePropertyFlags.Identifier | ObjectTypePropertyFlags.Required)]
32+
public required string Url { get; set; }
33+
34+
[TypeProperty("The path where the repository will be cloned in the Databricks workspace.", ObjectTypePropertyFlags.Identifier)]
35+
public string? Path { get; set; }
36+
}
37+
38+
[ResourceType("Repository")]
39+
public class Repository : RepositoryIdentifiers
40+
{
41+
[TypeProperty("The branch to checkout.")]
42+
public string? Branch { get; set; }
43+
44+
[TypeProperty("Sparse checkout configuration.")]
45+
public SparseCheckout? SparseCheckout { get; set; }
46+
47+
// Outputs - ReadOnly properties
48+
[TypeProperty("The unique identifier of the repository.", ObjectTypePropertyFlags.ReadOnly)]
49+
public string? Id { get; set; }
50+
51+
[TypeProperty("The head commit ID of the checked out branch.", ObjectTypePropertyFlags.ReadOnly)]
52+
public string? HeadCommitId { get; set; }
53+
}

src/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
isSingleton: true,
1515
typeAssembly: typeof(Program).Assembly,
1616
configurationType: typeof(Configuration))
17-
.WithResourceHandler<DatabricksGitCredentialHandler>();
17+
.WithResourceHandler<DatabricksGitCredentialHandler>()
18+
.WithResourceHandler<DatabricksRepositoryHandler>();
1819

1920
var app = builder.Build();
2021
app.MapBicepExtension();

0 commit comments

Comments
 (0)