diff --git a/src/GitLabApiClient/EnvironmentClient.cs b/src/GitLabApiClient/EnvironmentClient.cs
new file mode 100644
index 00000000..1bcc2f36
--- /dev/null
+++ b/src/GitLabApiClient/EnvironmentClient.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using GitLabApiClient.Internal.Http;
+using GitLabApiClient.Internal.Paths;
+using GitLabApiClient.Internal.Queries;
+using GitLabApiClient.Internal.Utilities;
+using GitLabApiClient.Models.Projects.Responses;
+using GitLabApiClient.Models.Environments.Requests;
+using GitLabApiClient.Models.Environments.Responses;
+using Environment = GitLabApiClient.Models.Environments.Responses.Environment;
+
+namespace GitLabApiClient
+{
+ public sealed class EnvironmentClient : IEnvironmentsClient
+ {
+ private readonly GitLabHttpFacade _httpFacade;
+ private readonly EnvironmentsQueryBuilder _environmentQueryBuilder;
+
+ internal EnvironmentClient(
+ GitLabHttpFacade httpFacade,
+ EnvironmentsQueryBuilder environmentQueryBuilder)
+ {
+ _httpFacade = httpFacade;
+ _environmentQueryBuilder = environmentQueryBuilder;
+ }
+
+ ///
+ /// Retrieves an environment by its name
+ ///
+ /// The ID, path or of the project.
+ /// The ID of the environment to retrieve.
+ ///
+ public async Task GetAsync(ProjectId projectId, int environmentId) =>
+ await _httpFacade.Get($"projects/{projectId}/environments/{environmentId}");
+
+ ///
+ /// Get a list of environments
+ ///
+ /// The ID, path or of the project.
+ /// Query options .
+ ///
+ public async Task> GetAsync(ProjectId projectId, Action options = null)
+ {
+ var queryOptions = new EnvironmentsQueryOptions();
+ options?.Invoke(queryOptions);
+
+ string url = _environmentQueryBuilder.Build($"projects/{projectId}/environments", queryOptions);
+ return await _httpFacade.GetPagedList(url);
+ }
+
+ ///
+ /// Create an environment
+ ///
+ /// The ID, path or of the project.
+ /// Create environment request.
+ ///
+ public async Task CreateAsync(ProjectId projectId, CreateEnvironmentRequest request) =>
+ await _httpFacade.Post($"projects/{projectId}/environments", request);
+
+ ///
+ /// Update an environment
+ ///
+ /// The ID, path or of the project.
+ /// Update environment request
+ ///
+ public async Task UpdateAsync(ProjectId projectId, UpdateEnvironmentRequest request) =>
+ await _httpFacade.Put($"projects/{projectId}/environments/{request.EnvironmentId}", request);
+
+ ///
+ /// Stop an environment
+ ///
+ /// The ID, path or of the project.
+ /// The ID of the environment to stop.
+ ///
+ public async Task StopAsync(ProjectId projectId, int environmentId) =>
+ await _httpFacade.Post($"projects/{projectId}/environments/{environmentId}/stop");
+
+ ///
+ /// Delete an environment
+ ///
+ /// The ID, path or of the project.
+ /// The ID of the environment to delete.
+ ///
+ public async Task DeleteAsync(ProjectId projectId, int environmentId) =>
+ await _httpFacade.Delete($"projects/{projectId}/environments/{environmentId}");
+ }
+}
diff --git a/src/GitLabApiClient/GitLabApiClient.csproj b/src/GitLabApiClient/GitLabApiClient.csproj
index 374b1209..ae3b6e98 100644
--- a/src/GitLabApiClient/GitLabApiClient.csproj
+++ b/src/GitLabApiClient/GitLabApiClient.csproj
@@ -19,7 +19,7 @@
- true
+ true
true
diff --git a/src/GitLabApiClient/GitLabClient.cs b/src/GitLabApiClient/GitLabClient.cs
index d64f7c34..7a746d25 100644
--- a/src/GitLabApiClient/GitLabClient.cs
+++ b/src/GitLabApiClient/GitLabClient.cs
@@ -60,6 +60,7 @@ public GitLabClient(string hostUrl, string authenticationToken = "", HttpMessage
var jobQueryBuilder = new JobQueryBuilder();
var toDoListBuilder = new ToDoListQueryBuilder();
var iterationsBuilder = new IterationsQueryBuilder();
+ var environmentBuilder = new EnvironmentsQueryBuilder();
Issues = new IssuesClient(_httpFacade, issuesQueryBuilder, projectIssueNotesQueryBuilder);
Uploads = new UploadsClient(_httpFacade);
@@ -80,6 +81,7 @@ public GitLabClient(string hostUrl, string authenticationToken = "", HttpMessage
ToDoList = new ToDoListClient(_httpFacade, toDoListBuilder);
Iterations = new IterationsClient(_httpFacade, iterationsBuilder);
Connection = new ConnectionClient(_httpFacade);
+ Environments = new EnvironmentClient(_httpFacade, environmentBuilder);
}
///
@@ -175,6 +177,11 @@ public GitLabClient(string hostUrl, string authenticationToken = "", HttpMessage
///
public IIterationsClient Iterations { get; }
+ ///
+ /// Access GitLab's Environments API.
+ ///
+ public IEnvironmentsClient Environments { get; }
+
///
/// Host address of GitLab instance. For example https://gitlab.example.com or https://gitlab.example.com/api/v4/.
///
diff --git a/src/GitLabApiClient/IEnvironmentsClient.cs b/src/GitLabApiClient/IEnvironmentsClient.cs
new file mode 100644
index 00000000..8c0f4938
--- /dev/null
+++ b/src/GitLabApiClient/IEnvironmentsClient.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using GitLabApiClient.Internal.Paths;
+using GitLabApiClient.Models.Projects.Responses;
+using GitLabApiClient.Models.Environments.Requests;
+using GitLabApiClient.Models.Environments.Responses;
+using Environment = GitLabApiClient.Models.Environments.Responses.Environment;
+
+namespace GitLabApiClient
+{
+ public interface IEnvironmentsClient
+ {
+ ///
+ /// Get a list of environments
+ ///
+ /// The ID, path or of the project.
+ /// The ID of the environment to retrieve.
+ ///
+ Task GetAsync(ProjectId projectId, int environmentId);
+
+ ///
+ /// Get a list of environments
+ ///
+ /// The ID, path or of the project.
+ /// Query options .
+ ///
+ Task> GetAsync(ProjectId projectId, Action options = null);
+
+ ///
+ /// Create an environment
+ ///
+ /// The ID, path or of the project.
+ /// Create environment request.
+ ///
+ Task CreateAsync(ProjectId projectId, CreateEnvironmentRequest request);
+
+ ///
+ /// Update an environment
+ ///
+ /// The ID, path or of the project.
+ /// Update environment request
+ ///
+ Task UpdateAsync(ProjectId projectId, UpdateEnvironmentRequest request);
+
+ ///
+ /// Stop an environment
+ ///
+ /// The ID, path or of the project.
+ /// The ID of the environment to stop.
+ ///
+ Task StopAsync(ProjectId projectId, int environmentId);
+
+ ///
+ /// Delete an environment
+ ///
+ /// The ID, path or of the project.
+ /// The ID of the environment to delete.
+ ///
+ Task DeleteAsync(ProjectId projectId, int environmentId);
+ }
+}
diff --git a/src/GitLabApiClient/Internal/Queries/EnvironmentsQueryBuilder.cs b/src/GitLabApiClient/Internal/Queries/EnvironmentsQueryBuilder.cs
new file mode 100644
index 00000000..efdea90e
--- /dev/null
+++ b/src/GitLabApiClient/Internal/Queries/EnvironmentsQueryBuilder.cs
@@ -0,0 +1,23 @@
+using System;
+using GitLabApiClient.Models.Environments.Requests;
+
+namespace GitLabApiClient.Internal.Queries
+{
+ class EnvironmentsQueryBuilder : QueryBuilder
+ {
+ protected override void BuildCore(Query query, EnvironmentsQueryOptions options)
+ {
+ if (!string.IsNullOrEmpty(options.Name) && !string.IsNullOrEmpty(options.Search))
+ throw new InvalidOperationException("Environment queries for 'name' and 'search' are mutually exclusive.");
+
+ if (!string.IsNullOrEmpty(options.Name))
+ query.Add("name", options.Name);
+
+ if (!string.IsNullOrEmpty(options.Search))
+ query.Add("search", options.Search);
+
+ if (options.States != null)
+ query.Add("states", options.States.ToString().ToLowerInvariant());
+ }
+ }
+}
diff --git a/src/GitLabApiClient/Models/Environments/Requests/CreateEnvironmentRequest.cs b/src/GitLabApiClient/Models/Environments/Requests/CreateEnvironmentRequest.cs
new file mode 100644
index 00000000..33c420f2
--- /dev/null
+++ b/src/GitLabApiClient/Models/Environments/Requests/CreateEnvironmentRequest.cs
@@ -0,0 +1,28 @@
+using System;
+using GitLabApiClient.Internal.Utilities;
+using GitLabApiClient.Models.Environments.Responses;
+using Newtonsoft.Json;
+using Environment = GitLabApiClient.Models.Environments.Responses.Environment;
+
+namespace GitLabApiClient.Models.Environments.Requests
+{
+ ///
+ /// Used to create an environment in a project.
+ ///
+ public sealed class CreateEnvironmentRequest : Environment
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the environment.
+ /// The external URL for the environment.
+ public CreateEnvironmentRequest(string name, Uri externalUrl)
+ {
+ Guard.NotEmpty(name, nameof(name));
+ Guard.NotNullOrDefault(externalUrl, nameof(externalUrl));
+
+ Name = name;
+ ExternalUrl = externalUrl;
+ }
+ }
+}
diff --git a/src/GitLabApiClient/Models/Environments/Requests/EnvironmentsQueryOptions.cs b/src/GitLabApiClient/Models/Environments/Requests/EnvironmentsQueryOptions.cs
new file mode 100644
index 00000000..387e8f82
--- /dev/null
+++ b/src/GitLabApiClient/Models/Environments/Requests/EnvironmentsQueryOptions.cs
@@ -0,0 +1,17 @@
+using GitLabApiClient.Models.Environments.Responses;
+
+namespace GitLabApiClient.Models.Environments.Requests
+{
+ public sealed class EnvironmentsQueryOptions
+ {
+ public string Name { get; set; }
+
+ public string Search { get; set; }
+
+ public EnvironmentState? States { get; set; }
+
+ internal EnvironmentsQueryOptions()
+ {
+ }
+ }
+}
diff --git a/src/GitLabApiClient/Models/Environments/Requests/UpdateEnvironmentRequest.cs b/src/GitLabApiClient/Models/Environments/Requests/UpdateEnvironmentRequest.cs
new file mode 100644
index 00000000..ac7d330c
--- /dev/null
+++ b/src/GitLabApiClient/Models/Environments/Requests/UpdateEnvironmentRequest.cs
@@ -0,0 +1,32 @@
+using System;
+using GitLabApiClient.Internal.Utilities;
+using Newtonsoft.Json;
+
+namespace GitLabApiClient.Models.Environments.Requests
+{
+ ///
+ /// Used to update an environment in a project
+ ///
+ public sealed class UpdateEnvironmentRequest
+ {
+ [JsonProperty("environment_id")]
+ public int EnvironmentId { get; set; }
+
+ [JsonProperty("external_url")]
+ public Uri ExternalUrl { get; set; }
+
+ ///
+ /// Initializes a new instance of the class
+ ///
+ /// The ID of the environment
+ /// The new external URL
+ public UpdateEnvironmentRequest(int environmentId, Uri externalUrl)
+ {
+ Guard.NotNullOrDefault(environmentId, nameof(environmentId));
+ Guard.NotNullOrDefault(externalUrl, nameof(externalUrl));
+
+ EnvironmentId = environmentId;
+ ExternalUrl = externalUrl;
+ }
+ }
+}
diff --git a/src/GitLabApiClient/Models/Environments/Responses/Environment.cs b/src/GitLabApiClient/Models/Environments/Responses/Environment.cs
new file mode 100644
index 00000000..5f562c79
--- /dev/null
+++ b/src/GitLabApiClient/Models/Environments/Responses/Environment.cs
@@ -0,0 +1,30 @@
+using System;
+using GitLabApiClient.Models.Environments.Responses;
+using Newtonsoft.Json;
+
+namespace GitLabApiClient.Models.Environments.Responses
+{
+ public class Environment
+ {
+ [JsonProperty("id")]
+ public int Id { get; set; }
+
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ [JsonProperty("slug")]
+ public string Slug { get; set; }
+
+ [JsonProperty("external_url")]
+ public Uri ExternalUrl { get; set; }
+
+ [JsonProperty("state")]
+ public EnvironmentState State { get; set; }
+
+ [JsonProperty("created_at")]
+ public DateTime? CreatedAt { get; set; }
+
+ [JsonProperty("updated_at")]
+ public DateTime? UpdatedAt { get; set; }
+ }
+}
diff --git a/src/GitLabApiClient/Models/Environments/Responses/EnvironmentState.cs b/src/GitLabApiClient/Models/Environments/Responses/EnvironmentState.cs
new file mode 100644
index 00000000..4e2ac8b7
--- /dev/null
+++ b/src/GitLabApiClient/Models/Environments/Responses/EnvironmentState.cs
@@ -0,0 +1,8 @@
+namespace GitLabApiClient.Models.Environments.Responses
+{
+ public enum EnvironmentState
+ {
+ Available,
+ Stopped
+ }
+}
diff --git a/test/GitLabApiClient.Test/EnvironmentsTest.cs b/test/GitLabApiClient.Test/EnvironmentsTest.cs
new file mode 100644
index 00000000..92ddc7d3
--- /dev/null
+++ b/test/GitLabApiClient.Test/EnvironmentsTest.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Threading.Tasks;
+using FluentAssertions;
+using GitLabApiClient.Internal.Queries;
+using GitLabApiClient.Models.Environments.Requests;
+using GitLabApiClient.Models.Environments.Responses;
+using Xunit;
+using static GitLabApiClient.Test.Utilities.GitLabApiHelper;
+using Environment = GitLabApiClient.Models.Environments.Responses.Environment;
+
+namespace GitLabApiClient.Test
+{
+ [Trait("Category", "LinuxIntegration")]
+ [Collection("GitLabContainerFixture")]
+ public class EnvironmentsTest
+ {
+ private readonly EnvironmentClient _sut = new EnvironmentClient(GetFacade(), new EnvironmentsQueryBuilder());
+
+ [Fact]
+ public async Task CreatedEnvironmentCanBeUpdated()
+ {
+ //arrange
+ const string testEnvironment = "Test Env Name";
+ var externalUrl = new Uri("https://dev.testingthis.com");
+ var createdEnvironment = await _sut.CreateAsync(TestProjectTextId,
+ new CreateEnvironmentRequest(testEnvironment, externalUrl));
+
+ //act
+ var updatedExternalUrl = new Uri("https://beta.testingthis.com");
+ var updatedEnvironment = await _sut.UpdateAsync(TestProjectTextId,
+ new UpdateEnvironmentRequest(createdEnvironment.Id, updatedExternalUrl));
+
+ //assert
+ updatedEnvironment.Should().Match(i =>
+ i.Name == testEnvironment &&
+ i.ExternalUrl == updatedExternalUrl);
+ }
+
+ [Fact]
+ public async Task CreatedEnvironmentCanBeFetched()
+ {
+ //arrange
+ const string testEnvironment = "Test Env Name";
+ var externalUrl = new Uri("https://dev.testingthis.com");
+ var createdEnvironment = await _sut.CreateAsync(TestProjectTextId,
+ new CreateEnvironmentRequest(testEnvironment, externalUrl));
+
+ //act
+ var fetchedEnvironment = await _sut.GetAsync(TestProjectTextId, createdEnvironment.Id);
+
+ //assert
+ fetchedEnvironment.Should().Match(i =>
+ i.Name == testEnvironment &&
+ i.ExternalUrl == externalUrl);
+ }
+
+ [Fact]
+ public async Task CreatedEnvironmentCanBeListed()
+ {
+ //arrange
+ const string testEnvironment = "Test Env Name";
+ var externalUrl = new Uri("https://dev.testingthis.com");
+ var createdEnvironment = await _sut.CreateAsync(TestProjectTextId,
+ new CreateEnvironmentRequest(testEnvironment, externalUrl));
+
+ //act
+ var environmentList = await _sut.GetAsync(TestProjectTextId);
+
+ //assert
+ environmentList.Should().Contain(i =>
+ i.Name == testEnvironment &&
+ i.ExternalUrl == externalUrl);
+ }
+
+ [Fact]
+ public async Task CreatedEnvironmentCanBeStopped()
+ {
+ //arrange
+ const string testEnvironment = "Test Env Name";
+ var externalUrl = new Uri("https://dev.testingthis.com");
+ var createdEnvironment = await _sut.CreateAsync(TestProjectTextId,
+ new CreateEnvironmentRequest(testEnvironment, externalUrl));
+
+ //act
+ await _sut.StopAsync(TestProjectTextId, createdEnvironment.Id);
+
+ //assert
+ var fetchedEnvironment = await _sut.GetAsync(TestProjectTextId);
+ fetchedEnvironment.Should().Contain(i =>
+ i.Name == testEnvironment &&
+ i.ExternalUrl == externalUrl &&
+ i.State == EnvironmentState.Stopped);
+ }
+
+ [Fact]
+ public async Task CreatedEnvironmentCanBeDeleted()
+ {
+ //arrange
+ const string testEnvironment = "Test Env Name";
+ var externalUrl = new Uri("https://dev.testingthis.com");
+ var createdEnvironment = await _sut.CreateAsync(TestProjectTextId,
+ new CreateEnvironmentRequest(testEnvironment, externalUrl));
+
+ //act
+ await _sut.DeleteAsync(TestProjectTextId, createdEnvironment.Id);
+
+ //assert
+ var fetchedEnvironment = await _sut.GetAsync(TestProjectTextId);
+ fetchedEnvironment.Should().BeEmpty();
+ }
+ }
+}
diff --git a/test/GitLabApiClient.Test/Internal/Queries/EnvironmentsQueryBuilderTest.cs b/test/GitLabApiClient.Test/Internal/Queries/EnvironmentsQueryBuilderTest.cs
new file mode 100644
index 00000000..61aa4738
--- /dev/null
+++ b/test/GitLabApiClient.Test/Internal/Queries/EnvironmentsQueryBuilderTest.cs
@@ -0,0 +1,64 @@
+using System;
+using FluentAssertions;
+using GitLabApiClient.Internal.Queries;
+using GitLabApiClient.Models.Environments.Requests;
+using GitLabApiClient.Models.Environments.Responses;
+using Xunit;
+
+namespace GitLabApiClient.Test.Internal.Queries
+{
+ public class EnvironmentsQueryBuilderTest
+ {
+ [Fact]
+ public void NameQueryBuilt()
+ {
+ var sut = new EnvironmentsQueryBuilder();
+
+ string query = sut.Build(
+ "https://gitlab.com/api/v4/projects/projectId/environments",
+ new EnvironmentsQueryOptions()
+ {
+ Name = "Test Env Name",
+ States = EnvironmentState.Available
+ });
+
+ query.Should().Be("https://gitlab.com/api/v4/projects/projectId/environments?" +
+ "name=Test%20Env%20Name&" +
+ "states=available");
+ }
+
+ [Fact]
+ public void SearchQueryBuilt()
+ {
+ var sut = new EnvironmentsQueryBuilder();
+
+ string query = sut.Build(
+ "https://gitlab.com/api/v4/projects/projectId/environments",
+ new EnvironmentsQueryOptions()
+ {
+ States = EnvironmentState.Available,
+ Search = "filter env"
+ });
+
+ query.Should().Be("https://gitlab.com/api/v4/projects/projectId/environments?" +
+ "search=filter%20env&" +
+ "states=available");
+ }
+
+ [Fact]
+ public void NameAndSearchMutuallyExclusive()
+ {
+ var sut = new EnvironmentsQueryBuilder();
+
+ Assert.Throws(()=>
+ sut.Build(
+ "https://gitlab.com/api/v4/projects/projectId/environments",
+ new EnvironmentsQueryOptions()
+ {
+ Name = "Test Env Name",
+ States = EnvironmentState.Available,
+ Search = "filter env"
+ }));
+ }
+ }
+}