diff --git a/abstractions/Directory.Build.props b/abstractions/Directory.Build.props
new file mode 100644
index 0000000000..7b8bd0a7dd
--- /dev/null
+++ b/abstractions/Directory.Build.props
@@ -0,0 +1,14 @@
+
+
+
+ canary
+ 0.1
+
+ latest
+ true
+
+
+
+
+
+
\ No newline at end of file
diff --git a/abstractions/OpenSearch.svg b/abstractions/OpenSearch.svg
new file mode 100644
index 0000000000..ad4c18df2d
--- /dev/null
+++ b/abstractions/OpenSearch.svg
@@ -0,0 +1 @@
+
diff --git a/abstractions/README.md b/abstractions/README.md
new file mode 100644
index 0000000000..2765381131
--- /dev/null
+++ b/abstractions/README.md
@@ -0,0 +1,79 @@
+
+
+- [OpenSearch .NET abstractions](#opensearch-net-abstractions)
+ - [OpenSearch.OpenSearch.Managed](#opensearchopensearchmanaged)
+ - [OpenSearch.OpenSearch.Ephemeral](#opensearchopensearchephemeral)
+ - [OpenSearch.OpenSearch.Xunit](#opensearchopensearchxunit)
+ - [OpenSearch.Stack.ArtifactsApi](#opensearchstackartifactsapi)
+- [Project Resources](#project-resources)
+- [Code of Conduct](#code-of-conduct)
+- [Security](#security)
+- [License](#license)
+- [Copyright](#copyright)
+
+## Welcome!
+
+# OpenSearch .NET abstractions
+
+You've reached the home repository for several auxiliary projects from the .NET team within OpenSearch.
+
+Pre-release packages of these projects are available at [OpenSearch NuGet Package Repository](https://www.github.com/opensearch-project/packages). See [GitHub documentation](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-nuget-registry#installing-a-package) for instructions on how to access them.
+
+Current projects:
+
+### [OpenSearch.OpenSearch.Managed](src/OpenSearch.OpenSearch.Managed/README.md)
+
+Provides an easy to start/stop one or more OpenSearch instances that exists on disk already
+
+### [OpenSearch.OpenSearch.Ephemeral](src/OpenSearch.OpenSearch.Ephemeral/README.md)
+
+Bootstrap (download, install, configure) and run OpenSearch clusters with ease.
+Started nodes are run in a new ephemeral location each time they are started and will clean up after they
+are disposed.
+
+### [OpenSearch.OpenSearch.Xunit](src/OpenSearch.OpenSearch.Xunit/README.md)
+
+Write integration tests against OpenSearch.
+Works with `.NET Core` and `.NET 4.6` and up.
+
+Supports `dotnet xunit`, `dotnet test`, `xunit.console.runner` and tests will be runnable in your IDE through VSTest and jetBrains Rider.
+
+### [OpenSearch.Stack.ArtifactsApi](src/OpenSearch.Stack.ArtifactsApi/README.md)
+
+Library to fetch the url and metadata for released artifacts.
+
+Supports:
+
+1. Snapshots builds
+ * `latest-MAJOR` where `MAJOR` is a single integer representing the major you want
+ * `latest` latest greatest
+
+2. Released versions
+ * `MAJOR.MINOR.PATH` where `MAJOR` is still supported as defined by the EOL policy of OpenSearch.
+ * Note if the version exists but is not yet released it will resolve as a build candidate
+
+## Project Resources
+
+* [Project Website](https://opensearch.org/)
+* Need help? Try [Forums](https://discuss.opendistrocommunity.dev/)
+* [Project Principles](https://opensearch.org/#principles)
+* [Contributing to OpenSearch](CONTRIBUTING.md)
+* [Maintainer Responsibilities](MAINTAINERS.md)
+* [Release Management](RELEASING.md)
+* [Admin Responsibilities](ADMINS.md)
+* [Security](SECURITY.md)
+
+## Code of Conduct
+
+This project has adopted the [Amazon Open Source Code of Conduct](CODE_OF_CONDUCT.md). For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq), or contact [opensource-codeofconduct@amazon.com](mailto:opensource-codeofconduct@amazon.com) with any additional questions or comments.
+
+## Security
+If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) or directly via email to aws-security@amazon.com. Please do **not** create a public GitHub issue.
+
+## License
+
+This project is licensed under the [Apache v2.0 License](LICENSE.txt).
+
+## Copyright
+
+Copyright OpenSearch Contributors. See [NOTICE](NOTICE.txt) for details.
diff --git a/abstractions/build.bat b/abstractions/build.bat
new file mode 100644
index 0000000000..b8df2a0857
--- /dev/null
+++ b/abstractions/build.bat
@@ -0,0 +1,2 @@
+@echo off
+dotnet run --project build/scripts -- %*
diff --git a/abstractions/build.sh b/abstractions/build.sh
new file mode 100644
index 0000000000..8c4d9c1114
--- /dev/null
+++ b/abstractions/build.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+set -euo pipefail
+dotnet run --project build/scripts -- "$@"
diff --git a/abstractions/build/keys/keypair.snk b/abstractions/build/keys/keypair.snk
new file mode 100644
index 0000000000..6de0112e8e
Binary files /dev/null and b/abstractions/build/keys/keypair.snk differ
diff --git a/abstractions/build/keys/public.snk b/abstractions/build/keys/public.snk
new file mode 100644
index 0000000000..9c2e41d4a6
Binary files /dev/null and b/abstractions/build/keys/public.snk differ
diff --git a/abstractions/build/scripts/CommandLine.fs b/abstractions/build/scripts/CommandLine.fs
new file mode 100644
index 0000000000..bb984b2329
--- /dev/null
+++ b/abstractions/build/scripts/CommandLine.fs
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// The OpenSearch Contributors require contributions made to
+// this file be licensed under the Apache-2.0 license or a
+// compatible open source license.
+//
+// Modifications Copyright OpenSearch Contributors. See
+// GitHub history for details.
+//
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+module CommandLine
+
+open Argu
+open Microsoft.FSharp.Reflection
+
+type Arguments =
+ | [] Clean
+ | [] Build
+
+ | [] PristineCheck
+ | [] GeneratePackages
+ | [] ValidatePackages
+ | [] GenerateReleaseNotes
+ | [] GenerateApiChanges
+ | [] Release
+
+ | [] CreateReleaseOnGithub
+ | [] Publish
+
+ | [] SingleTarget of bool
+ | [] Token of string
+with
+ interface IArgParserTemplate with
+ member this.Usage =
+ match this with
+ | Clean _ -> "clean known output locations"
+ | Build _ -> "Run build and tests"
+ | Release _ -> "runs build, and create an validates the packages shy of publishing them"
+ | Publish _ -> "Runs the full release"
+
+ | SingleTarget _ -> "Runs the provided sub command without running their dependencies"
+ | Token _ -> "Token to be used to authenticate with github"
+
+ | PristineCheck
+ | GeneratePackages
+ | ValidatePackages
+ | GenerateReleaseNotes
+ | GenerateApiChanges
+ | CreateReleaseOnGithub
+ -> "Undocumented, dependent target"
+ member this.Name =
+ match FSharpValue.GetUnionFields(this, typeof) with
+ | case, _ -> case.Name.ToLowerInvariant()
diff --git a/abstractions/build/scripts/Paths.fs b/abstractions/build/scripts/Paths.fs
new file mode 100644
index 0000000000..d385b62526
--- /dev/null
+++ b/abstractions/build/scripts/Paths.fs
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// The OpenSearch Contributors require contributions made to
+// this file be licensed under the Apache-2.0 license or a
+// compatible open source license.
+//
+// Modifications Copyright OpenSearch Contributors. See
+// GitHub history for details.
+//
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+module Paths
+
+open System
+open System.IO
+
+let ToolName = "opensearch-net-abstractions"
+let Repository = sprintf "opensearch/%s" ToolName
+let MainTFM = "netstandard2.0"
+
+let ValidateAssemblyName = false
+let IncludeGitHashInInformational = true
+let GenerateApiChanges = false
+
+let Root =
+ let mutable dir = DirectoryInfo(".")
+ while dir.GetFiles("*.sln").Length = 0 do dir <- dir.Parent
+ Environment.CurrentDirectory <- dir.FullName
+ dir
+
+let RootRelative path = Path.GetRelativePath(Root.FullName, path)
+
+let Output = DirectoryInfo(Path.Combine(Root.FullName, "build", "output"))
+
+let ToolProject = DirectoryInfo(Path.Combine(Root.FullName, "src", ToolName))
diff --git a/abstractions/build/scripts/Program.fs b/abstractions/build/scripts/Program.fs
new file mode 100644
index 0000000000..759b1d1726
--- /dev/null
+++ b/abstractions/build/scripts/Program.fs
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// The OpenSearch Contributors require contributions made to
+// this file be licensed under the Apache-2.0 license or a
+// compatible open source license.
+//
+// Modifications Copyright OpenSearch Contributors. See
+// GitHub history for details.
+//
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+module Program
+
+open Argu
+open Bullseye
+open ProcNet
+open CommandLine
+
+[]
+let main argv =
+ let parser = ArgumentParser.Create(programName = "./build.sh")
+ let parsed =
+ try
+ let parsed = parser.ParseCommandLine(inputs = argv, raiseOnUsage = true)
+ let arguments = parsed.GetSubCommand()
+ Some (parsed, arguments)
+ with e ->
+ printfn "%s" e.Message
+ None
+
+ match parsed with
+ | None -> 2
+ | Some (parsed, arguments) ->
+
+ let target = arguments.Name
+
+ Targets.Setup parsed arguments
+ let swallowTypes = [typeof; typeof]
+
+ Targets.RunTargetsAndExit
+ ([target], (fun e -> swallowTypes |> List.contains (e.GetType()) ), ":")
+ 0
+
diff --git a/abstractions/build/scripts/Targets.fs b/abstractions/build/scripts/Targets.fs
new file mode 100644
index 0000000000..3b09cc92fd
--- /dev/null
+++ b/abstractions/build/scripts/Targets.fs
@@ -0,0 +1,183 @@
+// SPDX-License-Identifier: Apache-2.0
+//
+// The OpenSearch Contributors require contributions made to
+// this file be licensed under the Apache-2.0 license or a
+// compatible open source license.
+//
+// Modifications Copyright OpenSearch Contributors. See
+// GitHub history for details.
+//
+// Licensed to Elasticsearch B.V. under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Elasticsearch B.V. licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+module Targets
+
+open Argu
+open System
+open System.IO
+open Bullseye
+open CommandLine
+open Fake.Tools.Git
+open ProcNet
+
+
+let exec binary args =
+ let r = Proc.Exec (binary, args |> List.map (fun a -> sprintf "\"%s\"" a) |> List.toArray)
+ match r.HasValue with | true -> r.Value | false -> failwithf "invocation of `%s` timed out" binary
+
+let private restoreTools = lazy(exec "dotnet" ["tool"; "restore"])
+let private currentVersion =
+ lazy(
+ restoreTools.Value |> ignore
+ let r = Proc.Start("dotnet", "minver", "-d", "canary", "-m", "0.1")
+ let o = r.ConsoleOut |> Seq.find (fun l -> not(l.Line.StartsWith("MinVer:")))
+ o.Line
+ )
+let private currentVersionInformational =
+ lazy(
+ match Paths.IncludeGitHashInInformational with
+ | false -> currentVersion.Value
+ | true -> sprintf "%s+%s" currentVersion.Value (Information.getCurrentSHA1( "."))
+ )
+
+let private clean (arguments:ParseResults) =
+ if (Paths.Output.Exists) then Paths.Output.Delete (true)
+ exec "dotnet" ["clean"] |> ignore
+
+let private build (arguments:ParseResults) = exec "dotnet" ["build"; "-c"; "Release"] |> ignore
+
+let private pristineCheck (arguments:ParseResults) =
+ match Information.isCleanWorkingCopy "." with
+ | true -> printfn "The checkout folder does not have pending changes, proceeding"
+ | _ -> failwithf "The checkout folder has pending changes, aborting"
+
+let private generatePackages (arguments:ParseResults) =
+ let output = Paths.RootRelative Paths.Output.FullName
+ exec "dotnet" ["pack"; "-c"; "Release"; "-o"; output] |> ignore
+
+let private validatePackages (arguments:ParseResults) =
+ let output = Paths.RootRelative <| Paths.Output.FullName
+ let nugetPackages =
+ Paths.Output.GetFiles("*.nupkg") |> Seq.sortByDescending(fun f -> f.CreationTimeUtc)
+ |> Seq.map (fun p -> Paths.RootRelative p.FullName)
+
+ let appVeyorArgs =
+ if Fake.Core.Environment.environVarAsBool "APPVEYOR" then ["-r"; "true"] else []
+
+ let args = ["-v"; currentVersionInformational.Value; "-k"; "96c599bbe3e70f5d"; "-t"; output] @ appVeyorArgs
+ nugetPackages |> Seq.iter (fun p -> exec "dotnet" (["nupkg-validator"; p] @ args) |> ignore)
+
+
+let private generateApiChanges (arguments:ParseResults) =
+ let output = Paths.RootRelative <| Paths.Output.FullName
+ let currentVersion = currentVersion.Value
+ let nugetPackages =
+ Paths.Output.GetFiles("*.nupkg") |> Seq.sortByDescending(fun f -> f.CreationTimeUtc)
+ |> Seq.map (fun p -> Path.GetFileNameWithoutExtension(Paths.RootRelative p.FullName).Replace("." + currentVersion, ""))
+ nugetPackages
+ |> Seq.iter(fun p ->
+ let outputFile =
+ let f = sprintf "breaking-changes-%s.md" p
+ Path.Combine(output, f)
+ let args =
+ [
+ "assembly-differ"
+ (sprintf "previous-nuget|%s|%s|%s" p currentVersion Paths.MainTFM);
+ (sprintf "directory|src/%s/bin/Release/%s" p Paths.MainTFM);
+ "-a"; "true"; "--target"; p; "-f"; "github-comment"; "--output"; outputFile
+ ]
+
+ exec "dotnet" args |> ignore
+ )
+
+let private generateReleaseNotes (arguments:ParseResults) =
+ let currentVersion = currentVersion.Value
+ let output =
+ Paths.RootRelative <| Path.Combine(Paths.Output.FullName, sprintf "release-notes-%s.md" currentVersion)
+ let tokenArgs =
+ match arguments.TryGetResult Token with
+ | None -> []
+ | Some token -> ["--token"; token;]
+ let releaseNotesArgs =
+ (Paths.Repository.Split("/") |> Seq.toList)
+ @ ["--version"; currentVersion
+ "--label"; "enhancement"; "New Features"
+ "--label"; "bug"; "Bug Fixes"
+ "--label"; "documentation"; "Docs Improvements"
+ ] @ tokenArgs
+ @ ["--output"; output]
+
+ exec "dotnet" (["release-notes"] @ releaseNotesArgs) |> ignore
+
+let private createReleaseOnGithub (arguments:ParseResults) =
+ let currentVersion = currentVersion.Value
+ let tokenArgs =
+ match arguments.TryGetResult Token with
+ | None -> []
+ | Some token -> ["--token"; token;]
+ let releaseNotes = Paths.RootRelative <| Path.Combine(Paths.Output.FullName, sprintf "release-notes-%s.md" currentVersion)
+ let breakingChanges =
+ let breakingChangesDocs = Paths.Output.GetFiles("breaking-changes-*.md")
+ breakingChangesDocs
+ |> Seq.map(fun f -> ["--body"; Paths.RootRelative f.FullName])
+ |> Seq.collect id
+ |> Seq.toList
+ let releaseArgs =
+ (Paths.Repository.Split("/") |> Seq.toList)
+ @ ["create-release"
+ "--version"; currentVersion
+ "--body"; releaseNotes;
+ ] @ breakingChanges @ tokenArgs
+
+ exec "dotnet" (["release-notes"] @ releaseArgs) |> ignore
+
+let private release (arguments:ParseResults) = printfn "release"
+
+let private publish (arguments:ParseResults) = printfn "publish"
+
+let Setup (parsed:ParseResults) (subCommand:Arguments) =
+ let step (name:string) action = Targets.Target(name, new Action(fun _ -> action(parsed)))
+
+ let cmd (name:string) commandsBefore steps action =
+ let singleTarget = (parsed.TryGetResult SingleTarget |> Option.defaultValue false)
+ let deps =
+ match (singleTarget, commandsBefore) with
+ | (true, _) -> []
+ | (_, Some d) -> d
+ | _ -> []
+ let steps = steps |> Option.defaultValue []
+ Targets.Target(name, deps @ steps, Action(action))
+
+ step Clean.Name clean
+ cmd Build.Name None (Some [Clean.Name]) <| fun _ -> build parsed
+
+ step PristineCheck.Name pristineCheck
+ step GeneratePackages.Name generatePackages
+ step ValidatePackages.Name validatePackages
+ step GenerateReleaseNotes.Name generateReleaseNotes
+ step GenerateApiChanges.Name generateApiChanges
+ cmd Release.Name
+ (Some [PristineCheck.Name; Build.Name;])
+ (Some [GeneratePackages.Name; ValidatePackages.Name; GenerateReleaseNotes.Name; GenerateApiChanges.Name])
+ <| fun _ -> release parsed
+
+ step CreateReleaseOnGithub.Name createReleaseOnGithub
+ cmd Publish.Name
+ (Some [Release.Name])
+ (Some [CreateReleaseOnGithub.Name; ])
+ <| fun _ -> publish parsed
diff --git a/abstractions/build/scripts/scripts.fsproj b/abstractions/build/scripts/scripts.fsproj
new file mode 100644
index 0000000000..b0377d43e3
--- /dev/null
+++ b/abstractions/build/scripts/scripts.fsproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net5.0
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abstractions/dotnet-tools.json b/abstractions/dotnet-tools.json
new file mode 100644
index 0000000000..6da03022c9
--- /dev/null
+++ b/abstractions/dotnet-tools.json
@@ -0,0 +1,30 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "minver-cli": {
+ "version": "2.3.1",
+ "commands": [
+ "minver"
+ ]
+ },
+ "assembly-differ": {
+ "version": "0.13.0",
+ "commands": [
+ "assembly-differ"
+ ]
+ },
+ "release-notes": {
+ "version": "0.3.0",
+ "commands": [
+ "release-notes"
+ ]
+ },
+ "nupkg-validator": {
+ "version": "0.4.0",
+ "commands": [
+ "nupkg-validator"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/abstractions/nuget-icon.png b/abstractions/nuget-icon.png
new file mode 100644
index 0000000000..edbf28ee83
Binary files /dev/null and b/abstractions/nuget-icon.png differ
diff --git a/abstractions/src/Directory.Build.props b/abstractions/src/Directory.Build.props
new file mode 100644
index 0000000000..968df46aea
--- /dev/null
+++ b/abstractions/src/Directory.Build.props
@@ -0,0 +1,39 @@
+
+
+
+
+ OpenSearch Project and contributors
+ OpenSearch
+ Apache-2.0
+ https://github.com/opensearch-project/opensearch-net-abstractions
+ Git
+ https://github.com/opensearch-project/opensearch-net-abstractions
+ https://github.com/opensearch-project/opensearch-net-abstractions/releases
+
+
+ true
+ ..\..\build\keys\keypair.snk
+
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+ true
+ nuget-icon.png
+
+
+
+
+
+ nuget-icon.png
+ True
+ nuget-icon.png
+
+
+ True
+ LICENSE.txt
+
+
+
+
+
+
+
+
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/ClusterAuthentication.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/ClusterAuthentication.cs
new file mode 100644
index 0000000000..5e041115cb
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/ClusterAuthentication.cs
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+namespace OpenSearch.OpenSearch.Ephemeral
+{
+ ///
+ /// Authentication credentials for the cluster
+ ///
+ public class ClusterAuthentication
+ {
+ ///
+ /// Administrator credentials
+ ///
+ public static Credentials Admin => new Credentials {Username = "admin", Role = "admin"};
+
+ ///
+ /// User credentials
+ ///
+ public static Credentials User => new Credentials {Username = "admin", Role = "admin"};
+
+ ///
+ /// Credentials for all users
+ ///
+ public static Credentials[] AllUsers { get; } = {Admin, User};
+
+ ///
+ /// Authentication credentials
+ ///
+ public class Credentials
+ {
+ public string Username { get; set; }
+ public string Role { get; set; }
+ public string Password => Username;
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/ClusterFeatures.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/ClusterFeatures.cs
new file mode 100644
index 0000000000..632270bf69
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/ClusterFeatures.cs
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System;
+
+namespace OpenSearch.OpenSearch.Ephemeral
+{
+ ///
+ /// Hints to what features the cluster to be started should have.
+ /// It's up to the to actually bootstrap these features.
+ ///
+ [Flags]
+ public enum ClusterFeatures
+ {
+ ///
+ /// No features
+ ///
+ None = 1 << 0,
+
+ ///
+ /// SSL/TLS for HTTP and Transport layers
+ ///
+ SSL = 1 << 3,
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/EphemeralCluster.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/EphemeralCluster.cs
new file mode 100644
index 0000000000..cc95c7d9f7
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/EphemeralCluster.cs
@@ -0,0 +1,132 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using OpenSearch.OpenSearch.Managed;
+using OpenSearch.Stack.ArtifactsApi;
+
+namespace OpenSearch.OpenSearch.Ephemeral
+{
+ public class EphemeralCluster : EphemeralCluster
+ {
+ public EphemeralCluster(OpenSearchVersion version, int numberOfNodes = 1)
+ : base(new EphemeralClusterConfiguration(version, ServerType.DEFAULT, ClusterFeatures.None, numberOfNodes: numberOfNodes))
+ {
+ }
+
+ public EphemeralCluster(EphemeralClusterConfiguration clusterConfiguration) : base(clusterConfiguration)
+ {
+ }
+ }
+
+ public abstract class EphemeralCluster : ClusterBase,
+ IEphemeralCluster
+ where TConfiguration : EphemeralClusterConfiguration
+ {
+ protected EphemeralCluster(TConfiguration clusterConfiguration) : base(clusterConfiguration) =>
+ Composer = new EphemeralClusterComposer(this);
+
+ protected EphemeralClusterComposer Composer { get; }
+
+ public virtual ICollection NodesUris(string hostName = null)
+ {
+ hostName = hostName ?? (ClusterConfiguration.HttpFiddlerAware && Process.GetProcessesByName("fiddler").Any()
+ ? "ipv4.fiddler"
+ : "localhost");
+ var ssl = ClusterConfiguration.EnableSsl ? "s" : "";
+ return Nodes
+ .Select(n => $"http{ssl}://{hostName}:{n.Port ?? 9200}")
+ .Distinct()
+ .Select(n => new Uri(n))
+ .ToList();
+ }
+
+ public bool CachingAndCachedHomeExists()
+ {
+ if (!ClusterConfiguration.CacheOpenSearchHomeInstallation) return false;
+ var cachedOpenSearchHomeFolder = Path.Combine(FileSystem.LocalFolder, GetCacheFolderName());
+ return Directory.Exists(cachedOpenSearchHomeFolder);
+ }
+
+ public virtual string GetCacheFolderName()
+ {
+ var config = ClusterConfiguration;
+
+ var sb = new StringBuilder();
+ sb.Append(EphemeralClusterComposerBase.InstallationTasks.Count());
+ sb.Append("-");
+ if (config.EnableSsl) sb.Append("ssl");
+ if (config.Plugins != null && config.Plugins.Count > 0)
+ {
+ sb.Append("-");
+ foreach (var p in config.Plugins.OrderBy(p => p.SubProductName))
+ sb.Append(p.SubProductName.ToLowerInvariant());
+ }
+
+ var name = sb.ToString();
+
+ return CalculateSha1(name, Encoding.UTF8);
+ }
+
+ protected override void OnBeforeStart()
+ {
+ Composer.Install();
+ Composer.OnBeforeStart();
+ }
+
+ protected override void OnDispose() => Composer.OnStop();
+
+ protected override void OnAfterStarted() => Composer.OnAfterStart();
+
+ protected override string SeeLogsMessage(string message)
+ {
+ var log = Path.Combine(FileSystem.LogsPath, $"{ClusterConfiguration.ClusterName}.log");
+ if (!File.Exists(log) || ClusterConfiguration.ShowOpenSearchOutputAfterStarted) return message;
+ if (!Started) return message;
+ using (var fileStream = new FileStream(log, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ using (var textReader = new StreamReader(fileStream))
+ {
+ var logContents = textReader.ReadToEnd();
+ return message + $" contents of {log}:{Environment.NewLine}" + logContents;
+ }
+ }
+
+ public static string CalculateSha1(string text, Encoding enc)
+ {
+ var buffer = enc.GetBytes(text);
+ var cryptoTransformSha1 = new SHA1CryptoServiceProvider();
+ return BitConverter.ToString(cryptoTransformSha1.ComputeHash(buffer))
+ .Replace("-", "").ToLowerInvariant().Substring(0, 12);
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/EphemeralClusterComposer.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/EphemeralClusterComposer.cs
new file mode 100644
index 0000000000..c245935598
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/EphemeralClusterComposer.cs
@@ -0,0 +1,134 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System;
+using System.Collections.Generic;
+using OpenSearch.OpenSearch.Ephemeral.Tasks;
+using OpenSearch.OpenSearch.Ephemeral.Tasks.AfterNodeStoppedTasks;
+using OpenSearch.OpenSearch.Ephemeral.Tasks.BeforeStartNodeTasks;
+using OpenSearch.OpenSearch.Ephemeral.Tasks.InstallationTasks;
+using OpenSearch.OpenSearch.Ephemeral.Tasks.ValidationTasks;
+using OpenSearch.OpenSearch.Managed.FileSystem;
+
+namespace OpenSearch.OpenSearch.Ephemeral
+{
+ public class EphemeralClusterComposerBase
+ {
+ protected EphemeralClusterComposerBase()
+ {
+ }
+
+ internal static IEnumerable InstallationTasks { get; } = new List
+ {
+ new PrintConfiguration(),
+ new CreateLocalApplicationDirectory(),
+ new CopyCachedOpenSearchInstallation(),
+ new EnsureJavaHomeEnvironmentVariableIsSet(),
+ new DownloadOpenSearchVersion(),
+ new UnzipOpenSearch(),
+ new SetOpenSearchBundledJdkJavaHome(),
+ new InstallPlugins(),
+ new InitialConfiguration()
+ };
+
+ protected static IEnumerable BeforeStart { get; } = new List
+ {
+ new CreateEphemeralDirectory(),
+ new CacheOpenSearchInstallation()
+ };
+
+ protected static IEnumerable NodeStoppedTasks { get; } = new List
+ {
+ new CleanUpDirectoriesAfterNodeStopped()
+ };
+
+ protected static IEnumerable AfterStartedTasks { get; } = new List
+ {
+ new ValidateRunningVersion(),
+ new ValidateClusterStateTask(),
+ new ValidatePluginsTask(),
+ };
+ }
+
+
+ public class EphemeralClusterComposer : EphemeralClusterComposerBase
+ where TConfiguration : EphemeralClusterConfiguration
+ {
+ private readonly object _lock = new object();
+ public EphemeralClusterComposer(IEphemeralCluster cluster) => Cluster = cluster;
+
+ private IEphemeralCluster Cluster { get; }
+
+ private bool NodeStarted { get; set; }
+
+ public void OnStop() => Itterate(NodeStoppedTasks, (t, c, fs) => t.Run(c, NodeStarted), false);
+
+ public void Install() => Itterate(InstallationTasks, (t, c, fs) => t.Run(c));
+
+ public void OnBeforeStart()
+ {
+ var tasks = new List(BeforeStart);
+ if (Cluster.ClusterConfiguration.AdditionalBeforeNodeStartedTasks != null)
+ tasks.AddRange(Cluster.ClusterConfiguration.AdditionalBeforeNodeStartedTasks);
+
+ if (Cluster.ClusterConfiguration.PrintYamlFilesInConfigFolder)
+ tasks.Add(new PrintYamlContents());
+
+ Itterate(tasks, (t, c, fs) => t.Run(c));
+
+ NodeStarted = true;
+ }
+
+ public void OnAfterStart()
+ {
+ if (Cluster.ClusterConfiguration.SkipBuiltInAfterStartTasks) return;
+ var tasks = new List(AfterStartedTasks);
+ if (Cluster.ClusterConfiguration.AdditionalAfterStartedTasks != null)
+ tasks.AddRange(Cluster.ClusterConfiguration.AdditionalAfterStartedTasks);
+ Itterate(tasks, (t, c, fs) => t.Run(c), false);
+ }
+
+ private void Itterate(IEnumerable collection,
+ Action, INodeFileSystem> act, bool callOnStop = true)
+ {
+ lock (_lock)
+ {
+ var cluster = Cluster;
+ foreach (var task in collection)
+ try
+ {
+ act(task, cluster, cluster.FileSystem);
+ }
+ catch (Exception)
+ {
+ if (callOnStop) OnStop();
+ throw;
+ }
+ }
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/EphemeralClusterConfiguration.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/EphemeralClusterConfiguration.cs
new file mode 100644
index 0000000000..181a569829
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/EphemeralClusterConfiguration.cs
@@ -0,0 +1,101 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using OpenSearch.OpenSearch.Ephemeral.Plugins;
+using OpenSearch.OpenSearch.Ephemeral.Tasks;
+using OpenSearch.OpenSearch.Managed.Configuration;
+using OpenSearch.Stack.ArtifactsApi;
+using OpenSearch.Stack.ArtifactsApi.Products;
+
+namespace OpenSearch.OpenSearch.Ephemeral
+{
+ public class EphemeralClusterConfiguration : ClusterConfiguration
+ {
+ public EphemeralClusterConfiguration(OpenSearchVersion version, ServerType serverType, OpenSearchPlugins plugins = null,
+ int numberOfNodes = 1)
+ : this(version, serverType, ClusterFeatures.None, plugins, numberOfNodes)
+ {
+ }
+
+ public EphemeralClusterConfiguration(OpenSearchVersion version, ServerType serverType, ClusterFeatures features,
+ OpenSearchPlugins plugins = null, int numberOfNodes = 1)
+ : base(version, serverType, (v, s) => new EphemeralFileSystem(v, s), numberOfNodes, EphemeralClusterName)
+ {
+ Features = features;
+
+ var pluginsList = plugins?.ToList() ?? new List();
+ Plugins = new OpenSearchPlugins(pluginsList);
+ }
+
+ private static string UniqueishSuffix => Guid.NewGuid().ToString("N").Substring(0, 6);
+ private static string EphemeralClusterName => $"ephemeral-cluster-{UniqueishSuffix}";
+
+ ///
+ /// The features supported by the cluster
+ ///
+ public ClusterFeatures Features { get; }
+
+ ///
+ /// The collection of plugins to install
+ ///
+ public OpenSearchPlugins Plugins { get; }
+
+ ///
+ /// Validates that the plugins to install can be installed on the target OpenSearch version.
+ /// This can be useful to fail early when subsequent operations are relying on installation
+ /// succeeding.
+ ///
+ public bool ValidatePluginsToInstall { get; } = true;
+
+ public bool EnableSsl => Features.HasFlag(ClusterFeatures.SSL);
+
+ public IList AdditionalBeforeNodeStartedTasks { get; } = new List();
+
+ public IList AdditionalAfterStartedTasks { get; } = new List();
+
+ ///
+ /// Expert level setting, skips all built-in validation tasks for cases where you need to guarantee your call is the
+ /// first call into the cluster
+ ///
+ public bool SkipBuiltInAfterStartTasks { get; set; }
+
+ /// Bootstrapping HTTP calls should attempt to auto route traffic through fiddler if its running
+ public bool HttpFiddlerAware { get; set; }
+
+ protected virtual string NodePrefix => "ephemeral";
+
+ public override string CreateNodeName(int? node)
+ {
+ var suffix = Guid.NewGuid().ToString("N").Substring(0, 6);
+ return $"{NodePrefix}-node-{suffix}{node}";
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/EphemeralFileSystem.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/EphemeralFileSystem.cs
new file mode 100644
index 0000000000..f27b6f6484
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/EphemeralFileSystem.cs
@@ -0,0 +1,86 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System.IO;
+using OpenSearch.OpenSearch.Managed.FileSystem;
+using OpenSearch.Stack.ArtifactsApi;
+using OpenSearch.Stack.ArtifactsApi.Products;
+
+namespace OpenSearch.OpenSearch.Ephemeral
+{
+ public class EphemeralFileSystem : NodeFileSystem
+ {
+ public EphemeralFileSystem(OpenSearchVersion version, string clusterName) : base(version,
+ EphemeralHome(version, clusterName)) => ClusterName = clusterName;
+
+ private string ClusterName { get; }
+
+ public string TempFolder => Path.Combine(Path.GetTempPath(), SubFolder, Artifact.LocalFolderName, ClusterName);
+
+ public override string ConfigPath => Path.Combine(TempFolder, "config");
+ public override string LogsPath => Path.Combine(TempFolder, "logs");
+ public override string RepositoryPath => Path.Combine(TempFolder, "repositories");
+ public override string DataPath => Path.Combine(TempFolder, "data");
+
+ //certificates
+ public string CertificateFolderName => "node-certificates";
+ public string CertificateNodeName => "node01";
+ public string ClientCertificateName => "cn=John Doe,ou=example,o=com";
+ public string ClientCertificateFilename => "john_doe";
+
+ public string CertificatesPath => Path.Combine(ConfigPath, CertificateFolderName);
+
+ public string CaCertificate => Path.Combine(CertificatesPath, "ca", "ca") + ".crt";
+
+ public string NodePrivateKey =>
+ Path.Combine(CertificatesPath, CertificateNodeName, CertificateNodeName) + ".key";
+
+ public string NodeCertificate =>
+ Path.Combine(CertificatesPath, CertificateNodeName, CertificateNodeName) + ".crt";
+
+ public string ClientCertificate =>
+ Path.Combine(CertificatesPath, ClientCertificateFilename, ClientCertificateFilename) + ".crt";
+
+ public string ClientPrivateKey =>
+ Path.Combine(CertificatesPath, ClientCertificateFilename, ClientCertificateFilename) + ".key";
+
+ public string UnusedCertificateFolderName => $"unused-{CertificateFolderName}";
+ public string UnusedCertificatesPath => Path.Combine(ConfigPath, UnusedCertificateFolderName);
+ public string UnusedCaCertificate => Path.Combine(UnusedCertificatesPath, "ca", "ca") + ".crt";
+
+ public string UnusedClientCertificate =>
+ Path.Combine(UnusedCertificatesPath, ClientCertificateFilename, ClientCertificateFilename) + ".crt";
+
+
+ protected static string EphemeralHome(OpenSearchVersion version, string clusterName)
+ {
+ var temp = Path.Combine(Path.GetTempPath(), SubFolder,
+ version.Artifact(Product.OpenSearch).LocalFolderName, clusterName);
+ return Path.Combine(temp, "home");
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/IEphemeralCluster.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/IEphemeralCluster.cs
new file mode 100644
index 0000000000..b5d9f8e7cc
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/IEphemeralCluster.cs
@@ -0,0 +1,45 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System;
+using System.Collections.Generic;
+using OpenSearch.OpenSearch.Managed;
+
+namespace OpenSearch.OpenSearch.Ephemeral
+{
+ public interface IEphemeralCluster
+ {
+ ICollection NodesUris(string hostName = null);
+ string GetCacheFolderName();
+ bool CachingAndCachedHomeExists();
+ }
+
+ public interface IEphemeralCluster : IEphemeralCluster, ICluster
+ where TConfiguration : EphemeralClusterConfiguration
+ {
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/OpenSearch.OpenSearch.Ephemeral.csproj b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/OpenSearch.OpenSearch.Ephemeral.csproj
new file mode 100644
index 0000000000..a4ec93b16c
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/OpenSearch.OpenSearch.Ephemeral.csproj
@@ -0,0 +1,15 @@
+
+
+
+ netstandard2.0;net461
+ Provides an EphemeralCluster implementation that can download/bootstrap/run a throwaway customizable OpenSearch cluster
+ opensearch,opensearch,cluster,ephemeral
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Plugins/OpenSearchPlugins.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Plugins/OpenSearchPlugins.cs
new file mode 100644
index 0000000000..3833aba1cf
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Plugins/OpenSearchPlugins.cs
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using OpenSearch.Stack.ArtifactsApi.Products;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Plugins
+{
+ public class OpenSearchPlugins : ReadOnlyCollection
+ {
+ public OpenSearchPlugins(IList list) : base(list)
+ {
+ }
+
+ public OpenSearchPlugins(params OpenSearchPlugin[] list) : base(list)
+ {
+ }
+
+ public static OpenSearchPlugins Supported { get; } =
+ new OpenSearchPlugins(new List
+ {
+ OpenSearchPlugin.AnalysisIcu,
+ OpenSearchPlugin.AnalysisKuromoji,
+ OpenSearchPlugin.AnalysisPhonetic,
+ OpenSearchPlugin.AnalysisSmartCn,
+ OpenSearchPlugin.AnalysisStempel,
+ OpenSearchPlugin.AnalysisUkrainian,
+ OpenSearchPlugin.DiscoveryAzureClassic,
+ OpenSearchPlugin.DiscoveryEC2,
+ OpenSearchPlugin.DiscoveryFile,
+ OpenSearchPlugin.DiscoveryGCE,
+ OpenSearchPlugin.IngestAttachment,
+ OpenSearchPlugin.IngestGeoIp,
+ OpenSearchPlugin.IngestUserAgent,
+ OpenSearchPlugin.MapperAttachment,
+ OpenSearchPlugin.MapperMurmur3,
+ OpenSearchPlugin.MapperSize,
+ OpenSearchPlugin.RepositoryAzure,
+ OpenSearchPlugin.RepositoryGCS,
+ OpenSearchPlugin.RepositoryHDFS,
+ OpenSearchPlugin.RepositoryS3,
+ OpenSearchPlugin.StoreSMB,
+ OpenSearchPlugin.DeleteByQuery,
+ });
+
+ public override string ToString() => string.Join(", ", Items.Select(s => s.SubProductName));
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/README.md b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/README.md
new file mode 100644
index 0000000000..39f5814d22
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/README.md
@@ -0,0 +1,60 @@
+# OpenSearch.Managed.Ephemeral
+
+Bootstrap (download, install, configure) and run OpenSearch clusters with ease.
+Started nodes are run in a new ephemeral location each time they are started and will clean up after they
+are disposed.
+
+
+## EphemeralCluster
+
+A `ClusterBase` implementation from `OpenSearch.Managed` that can:
+
+* download opensearch versions (stable releases, snapshots, build candidates)
+* download opensearch plugins (stable releases, snapshots, build candidates)
+* install opensearch and desired plugins in an ephemeral location. The source downloaded zips are cached
+on disk (LocalAppData).
+* Ships with builtin knowledge on how to enable SSL on the running cluster.
+* Start opensearch using ephemeral locations for OPENSEARCH_HOME and conf/logs/data paths.
+
+
+#### Examples:
+
+The easiest way to get started is by simply passing the version you want to be bootstrapped to `EphemeralCluster`.
+`Start` starts the `OpenSearchNode`'s and waits for them to be started. The default overload waits `2 minutes`.
+
+```csharp
+using (var cluster = new EphemeralCluster("1.0.0"))
+{
+ cluster.Start();
+}
+```
+
+If you want the full configuration possibilities inject a `EphemeralClusterConfiguration` instead:
+
+
+```csharp
+var plugins = new OpenSearchPlugins(OpenSearchPlugin.RepositoryAzure, OpenSearchPlugin.IngestAttachment);
+var config = new EphemeralClusterConfiguration("1.0.0", ServerType.OpenSearch, ClusterFeatures.None, plugins, numberOfNodes: 2);
+using (var cluster = new EphemeralCluster(config))
+{
+ cluster.Start();
+
+ var nodes = cluster.NodesUris();
+ var connectionPool = new StaticConnectionPool(nodes);
+ var settings = new ConnectionSettings(connectionPool).EnableDebugMode();
+ var client = new OpenSearchClient(settings);
+
+ Console.Write(client.CatPlugins().DebugInformation);
+}
+```
+Here we first create a `OpenSearchPlugins` collection of the plugins that we want to bootstrap.
+Then we create an instance of `EphemeralClusterConfiguration` that dictates we want a 2 node cluster
+running opensearch using the previous declared `plugins`.
+
+We then Start the node and after its up create a `OSC` client using the `NodeUris()` that the cluster
+started.
+
+We call `/_cat/plugins` and write `OSC`'s debug information to the console.
+
+When the cluster exits the using block and disposes the cluster all nodes will be shutdown gracefully.
+
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/SecurityRealms.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/SecurityRealms.cs
new file mode 100644
index 0000000000..d776863306
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/SecurityRealms.cs
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+namespace OpenSearch.OpenSearch.Ephemeral
+{
+ public static class SecurityRealms
+ {
+ public const string FileRealm = "file1";
+
+ public const string PkiRealm = "pki1";
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/AfterNodeStoppedTasks/CleanUpDirectoriesAfterNodeStopped.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/AfterNodeStoppedTasks/CleanUpDirectoriesAfterNodeStopped.cs
new file mode 100644
index 0000000000..e4cd466796
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/AfterNodeStoppedTasks/CleanUpDirectoriesAfterNodeStopped.cs
@@ -0,0 +1,83 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System.IO;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+using ProcNet.Std;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.AfterNodeStoppedTasks
+{
+ public class CleanUpDirectoriesAfterNodeStopped : IClusterTeardownTask
+ {
+ public void Run(IEphemeralCluster cluster, bool nodeStarted)
+ {
+ var fs = cluster.FileSystem;
+ var w = cluster.Writer;
+ var a = cluster.ClusterConfiguration.Artifact;
+ if (cluster.ClusterConfiguration.NoCleanupAfterNodeStopped)
+ {
+ w.WriteDiagnostic(
+ $"{{{nameof(CleanUpDirectoriesAfterNodeStopped)}}} skipping cleanup as requested on cluster configuration");
+ return;
+ }
+
+ DeleteDirectory(w, "cluster data", fs.DataPath);
+ DeleteDirectory(w, "cluster config", fs.ConfigPath);
+ DeleteDirectory(w, "cluster logs", fs.LogsPath);
+ DeleteDirectory(w, "repositories", fs.RepositoryPath);
+ var efs = fs as EphemeralFileSystem;
+ if (!string.IsNullOrWhiteSpace(efs?.TempFolder))
+ DeleteDirectory(w, "cluster temp folder", efs.TempFolder);
+
+ if (efs != null)
+ {
+ var extractedFolder = Path.Combine(fs.LocalFolder, a.FolderInZip);
+ if (extractedFolder != fs.OpenSearchHome)
+ DeleteDirectory(w, "ephemeral OPENSEARCH_HOME", fs.OpenSearchHome);
+ //if the node was not started delete the cached extractedFolder
+ if (!nodeStarted)
+ DeleteDirectory(w, "cached extracted folder - node failed to start", extractedFolder);
+ }
+
+ //if the node did not start make sure we delete the cached folder as we can not assume its in a good state
+ var cachedOpenSearchHomeFolder = Path.Combine(fs.LocalFolder, cluster.GetCacheFolderName());
+ if (cluster.ClusterConfiguration.CacheOpenSearchHomeInstallation && !nodeStarted)
+ DeleteDirectory(w, "cached installation - node failed to start", cachedOpenSearchHomeFolder);
+ else
+ w.WriteDiagnostic(
+ $"{{{nameof(CleanUpDirectoriesAfterNodeStopped)}}} Leaving [cached folder] on disk: {{{cachedOpenSearchHomeFolder}}}");
+ }
+
+ private static void DeleteDirectory(IConsoleLineHandler w, string description, string path)
+ {
+ if (!Directory.Exists(path)) return;
+ w.WriteDiagnostic(
+ $"{{{nameof(CleanUpDirectoriesAfterNodeStopped)}}} attempting to delete [{description}]: {{{path}}}");
+ Directory.Delete(path, true);
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/BeforeStartNodeTasks/CacheOpenSearchInstallation.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/BeforeStartNodeTasks/CacheOpenSearchInstallation.cs
new file mode 100644
index 0000000000..7bad124a23
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/BeforeStartNodeTasks/CacheOpenSearchInstallation.cs
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System.IO;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.BeforeStartNodeTasks
+{
+ public class CacheOpenSearchInstallation : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ if (!cluster.ClusterConfiguration.CacheOpenSearchHomeInstallation) return;
+
+ var fs = cluster.FileSystem;
+ var cachedOpenSearchHomeFolder = Path.Combine(fs.LocalFolder, cluster.GetCacheFolderName());
+ var cachedOpenSearchConfig = Path.Combine(cachedOpenSearchHomeFolder, "config");
+ if (File.Exists(cachedOpenSearchConfig))
+ {
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(CacheOpenSearchInstallation)}}} cached home already exists [{cachedOpenSearchHomeFolder}]");
+ return;
+ }
+
+ var source = fs.OpenSearchHome;
+ var target = cachedOpenSearchHomeFolder;
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(CacheOpenSearchInstallation)}}} caching {{{source}}} to [{target}]");
+ CopyFolder(source, target, false);
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/BeforeStartNodeTasks/CreateEphemeralDirectory.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/BeforeStartNodeTasks/CreateEphemeralDirectory.cs
new file mode 100644
index 0000000000..745f32ec45
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/BeforeStartNodeTasks/CreateEphemeralDirectory.cs
@@ -0,0 +1,84 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System.IO;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+using OpenSearch.OpenSearch.Managed.FileSystem;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.BeforeStartNodeTasks
+{
+ public class CreateEphemeralDirectory : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ var fs = cluster.FileSystem;
+ if (!(fs is EphemeralFileSystem f))
+ {
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(CreateEphemeralDirectory)}}} unexpected IFileSystem implementation {{{fs.GetType()}}}");
+ return;
+ }
+
+ cluster.Writer?.WriteDiagnostic($"{{{nameof(CreateEphemeralDirectory)}}} creating {{{f.TempFolder}}}");
+
+ Directory.CreateDirectory(f.TempFolder);
+
+ if (!Directory.Exists(f.ConfigPath))
+ {
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(CreateEphemeralDirectory)}}} creating config folder {{{f.ConfigPath}}}");
+ Directory.CreateDirectory(f.ConfigPath);
+ }
+
+ CopyHomeConfigToEphemeralConfig(cluster, f, fs);
+ }
+
+ private static void CopyHomeConfigToEphemeralConfig(IEphemeralCluster cluster,
+ EphemeralFileSystem ephemeralFileSystem, INodeFileSystem fs)
+ {
+ var target = ephemeralFileSystem.ConfigPath;
+ var cachedOpenSearchHomeFolder = Path.Combine(fs.LocalFolder, cluster.GetCacheFolderName());
+ var cachedOpenSearchYaml = Path.Combine(cachedOpenSearchHomeFolder, "config", "opensearch.yaml");
+
+ var homeSource =
+ cluster.ClusterConfiguration.CacheOpenSearchHomeInstallation && File.Exists(cachedOpenSearchYaml)
+ ? cachedOpenSearchHomeFolder
+ : fs.OpenSearchHome;
+ var source = Path.Combine(homeSource, "config");
+ if (!Directory.Exists(source))
+ {
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(CreateEphemeralDirectory)}}} source config {{{source}}} does not exist nothing to copy");
+ return;
+ }
+
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(CreateEphemeralDirectory)}}} copying cached {{{source}}} as to [{target}]");
+ CopyFolder(source, target);
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/BeforeStartNodeTasks/PrintYamlContents.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/BeforeStartNodeTasks/PrintYamlContents.cs
new file mode 100644
index 0000000000..c629294bee
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/BeforeStartNodeTasks/PrintYamlContents.cs
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System.IO;
+using System.Linq;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.BeforeStartNodeTasks
+{
+ public class PrintYamlContents : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ var c = cluster.ClusterConfiguration;
+ var v = c.Version;
+ var fs = cluster.FileSystem;
+
+ var files = Directory.GetFiles(fs.ConfigPath, "*.yml", SearchOption.AllDirectories);
+ foreach (var file in files) DumpFile(cluster, file);
+ }
+
+ private static void DumpFile(IEphemeralCluster cluster, string configFile)
+ {
+ if (!File.Exists(configFile))
+ {
+ cluster.Writer.WriteDiagnostic(
+ $"{{{nameof(PrintYamlContents)}}} skipped printing [{configFile}] as it does not exists");
+ return;
+ }
+
+ var fileName = Path.GetFileName(configFile);
+ cluster.Writer.WriteDiagnostic($"{{{nameof(PrintYamlContents)}}} printing [{configFile}]");
+ var lines = File.ReadAllLines(configFile).ToList();
+ foreach (var l in lines.Where(l => !string.IsNullOrWhiteSpace(l) && !l.StartsWith("#")))
+ cluster.Writer.WriteDiagnostic($"{{{nameof(PrintYamlContents)}}} [{fileName}] {l}");
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/IClusterComposeTask.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/IClusterComposeTask.cs
new file mode 100644
index 0000000000..1ba2581c74
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/IClusterComposeTask.cs
@@ -0,0 +1,269 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Security;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+using ICSharpCode.SharpZipLib.GZip;
+using ICSharpCode.SharpZipLib.Tar;
+using ProcNet;
+using ProcNet.Std;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks
+{
+ public interface IClusterComposeTask
+ {
+ void Run(IEphemeralCluster cluster);
+ }
+
+ public interface IClusterTeardownTask
+ {
+ ///
+ /// Called when the cluster disposes, used to clean up after itself.
+ ///
+ /// The cluster configuration of the node that was started
+ /// Whether the cluster composer was successful in starting the node
+ void Run(IEphemeralCluster cluster, bool nodeStarted);
+ }
+
+ public abstract class ClusterComposeTask : IClusterComposeTask
+ {
+ protected static bool IsWindows { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ protected static string BinarySuffix => IsWindows ? ".bat" : string.Empty;
+ public abstract void Run(IEphemeralCluster cluster);
+
+ protected static void DownloadFile(string from, string to)
+ {
+ if (File.Exists(to)) return;
+ var http = new HttpClient();
+ using (var stream = http.GetStreamAsync(new Uri(from)).GetAwaiter().GetResult())
+ using (var fileStream = File.Create(to))
+ {
+ stream.CopyTo(fileStream);
+ fileStream.Flush();
+ }
+ }
+
+ protected string GetResponseException(HttpResponseMessage m) =>
+ $"Code: {m?.StatusCode} Reason: {m?.ReasonPhrase} Content: {GetResponseString(m)}";
+
+ protected string GetResponseString(HttpResponseMessage m) =>
+ m?.Content?.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult() ?? string.Empty;
+
+ protected HttpResponseMessage Get(IEphemeralCluster cluster, string path,
+ string query) =>
+ Call(cluster, path, query, (c, u, t) => c.GetAsync(u, t));
+
+ protected HttpResponseMessage Post(IEphemeralCluster cluster, string path,
+ string query, string json) =>
+ Call(cluster, path, query,
+ (c, u, t) => c.PostAsync(u, new StringContent(json, Encoding.UTF8, "application/json"), t));
+
+ private HttpResponseMessage Call(
+ IEphemeralCluster cluster,
+ string path,
+ string query,
+ Func> verb)
+ {
+ var q = string.IsNullOrEmpty(query) ? "pretty=true" : query + "&pretty=true";
+ var statusUrl = new UriBuilder(cluster.NodesUris().First()) {Path = path, Query = q}.Uri;
+
+ var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(20));
+ var handler = new HttpClientHandler
+ {
+ AutomaticDecompression =
+ DecompressionMethods.Deflate | DecompressionMethods.GZip | DecompressionMethods.None,
+ };
+ cluster.Writer.WriteDiagnostic(
+ $"{{{nameof(Call)}}} [{statusUrl}] SSL: {cluster.ClusterConfiguration.EnableSsl}");
+ if (cluster.ClusterConfiguration.EnableSsl)
+ {
+#if !NETSTANDARD
+ ServicePointManager.ServerCertificateValidationCallback += ServerCertificateValidationCallback;
+#else
+ handler.ServerCertificateCustomValidationCallback += (m, c, cn, p) => true;
+#endif
+ }
+
+ using (var client = new HttpClient(handler) {Timeout = TimeSpan.FromSeconds(20)})
+ {
+ if (cluster.ClusterConfiguration.EnableSsl)
+ {
+ var byteArray =
+ Encoding.ASCII.GetBytes(
+ $"{ClusterAuthentication.Admin.Username}:{ClusterAuthentication.Admin.Password}");
+ client.DefaultRequestHeaders.Authorization =
+ new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
+ }
+
+ try
+ {
+ var response = verb(client, statusUrl, tokenSource.Token).ConfigureAwait(false).GetAwaiter()
+ .GetResult();
+ if (!response.IsSuccessStatusCode)
+ {
+ cluster.Writer.WriteDiagnostic(
+ $"{{{nameof(Call)}}} [{statusUrl}] Unsuccessful status code: [{(int) response.StatusCode}]");
+ var body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
+ foreach (var l in (body ?? string.Empty).Split('\n', '\r'))
+ cluster.Writer.WriteDiagnostic($"{{{nameof(Call)}}} [{statusUrl}] returned [{l}]");
+ }
+
+ return response;
+ }
+ catch (Exception e)
+ {
+ cluster.Writer.WriteError($"{{{nameof(Call)}}} [{statusUrl}] exception: {e}");
+ throw;
+ }
+ finally
+ {
+#if !NETSTANDARD
+ ServicePointManager.ServerCertificateValidationCallback -= ServerCertificateValidationCallback;
+#endif
+ }
+ }
+ }
+
+ private static bool ServerCertificateValidationCallback(object sender, X509Certificate certificate,
+ X509Chain chain, SslPolicyErrors sslpolicyerrors) => true;
+
+ protected static void WriteFileIfNotExist(string fileLocation, string contents)
+ {
+ if (!File.Exists(fileLocation)) File.WriteAllText(fileLocation, contents);
+ }
+
+ protected static void ExecuteBinary(EphemeralClusterConfiguration config, IConsoleLineHandler writer,
+ string binary, string description, params string[] arguments) =>
+ ExecuteBinaryInternal(config, writer, binary, description, null, arguments);
+
+ protected static void ExecuteBinary(EphemeralClusterConfiguration config, IConsoleLineHandler writer,
+ string binary, string description, StartedHandler startedHandler, params string[] arguments) =>
+ ExecuteBinaryInternal(config, writer, binary, description, startedHandler, arguments);
+
+ private static void ExecuteBinaryInternal(EphemeralClusterConfiguration config, IConsoleLineHandler writer,
+ string binary, string description, StartedHandler startedHandler, params string[] arguments)
+ {
+ var command = $"{{{binary}}} {{{string.Join(" ", arguments)}}}";
+ writer?.WriteDiagnostic($"{{{nameof(ExecuteBinary)}}} starting process [{description}] {command}");
+
+ var timeout = TimeSpan.FromSeconds(420);
+ var processStartArguments = new StartArguments(binary, arguments)
+ {
+ Environment = new Dictionary
+ {
+ {config.FileSystem.ConfigEnvironmentVariableName, config.FileSystem.ConfigPath},
+ {"OPENSEARCH_HOME", config.FileSystem.OpenSearchHome},
+ // Duplicate all env vars for ES_* prefix for backward compatibility with OpenDistro and ElasticSearch;
+ // unused env vars would be just ignored.
+ {config.FileSystem.ConfigEnvironmentVariableName.Replace("OPENSEARCH", "ES"), config.FileSystem.ConfigPath},
+ {"ES_HOME", config.FileSystem.OpenSearchHome}
+ }
+ };
+
+ var result = startedHandler != null
+ ? Proc.Start(processStartArguments, timeout, new ConsoleOutColorWriter(), startedHandler)
+ : Proc.Start(processStartArguments, timeout, new ConsoleOutColorWriter());
+
+ if (!result.Completed)
+ throw new Exception($"Timeout while executing {description} exceeded {timeout}");
+
+ if (result.ExitCode != 0)
+ throw new Exception(
+ $"Expected exit code 0 but received ({result.ExitCode}) while executing {description}: {command}");
+
+ var errorOut = result.ConsoleOut.Where(c => c.Error).ToList();
+
+ if (errorOut.Any(e =>
+ !string.IsNullOrWhiteSpace(e.Line) && !e.Line.Contains("usage of JAVA_HOME is deprecated")) &&
+ !binary.Contains("plugin") && !binary.Contains("cert"))
+ throw new Exception(
+ $"Received error out with exitCode ({result.ExitCode}) while executing {description}: {command}");
+
+ writer?.WriteDiagnostic(
+ $"{{{nameof(ExecuteBinary)}}} finished process [{description}] {{{result.ExitCode}}}");
+ }
+
+ protected static void CopyFolder(string source, string target, bool overwrite = true)
+ {
+ foreach (var sourceDir in Directory.GetDirectories(source, "*", SearchOption.AllDirectories))
+ {
+ var targetDir = sourceDir.Replace(source, target);
+ Directory.CreateDirectory(targetDir);
+ }
+
+ foreach (var sourcePath in Directory.GetFiles(source, "*.*", SearchOption.AllDirectories))
+ {
+ var targetPath = sourcePath.Replace(source, target);
+ if (!overwrite && File.Exists(targetPath)) continue;
+ File.Copy(sourcePath, targetPath, overwrite);
+ }
+ }
+
+ protected static void Extract(string file, string toFolder)
+ {
+ if (file.EndsWith(".zip")) ExtractZip(file, toFolder);
+ else if (file.EndsWith(".tar.gz")) ExtractTarGz(file, toFolder);
+ else if (file.EndsWith(".tar")) ExtractTar(file, toFolder);
+ else throw new Exception("Can not extract:" + file);
+ }
+
+ private static void ExtractTar(string file, string toFolder)
+ {
+ using (var inStream = File.OpenRead(file))
+ using (var tarArchive = TarArchive.CreateInputTarArchive(inStream))
+ tarArchive.ExtractContents(toFolder);
+ }
+
+ private static void ExtractTarGz(string file, string toFolder)
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ using (var inStream = File.OpenRead(file))
+ using (var gzipStream = new GZipInputStream(inStream))
+ using (var tarArchive = TarArchive.CreateInputTarArchive(gzipStream))
+ tarArchive.ExtractContents(toFolder);
+ else
+ //SharpZipLib loses permissions when untarring
+ Proc.Exec("tar", "-xvf", file, "-C", toFolder);
+ }
+
+ private static void ExtractZip(string file, string toFolder) =>
+ ZipFile.ExtractToDirectory(file, toFolder);
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/CopyCachedOpenSearchInstallation.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/CopyCachedOpenSearchInstallation.cs
new file mode 100644
index 0000000000..444e278f87
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/CopyCachedOpenSearchInstallation.cs
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System.IO;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.InstallationTasks
+{
+ public class CopyCachedOpenSearchInstallation : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ if (!cluster.ClusterConfiguration.CacheOpenSearchHomeInstallation) return;
+
+ var fs = cluster.FileSystem;
+ var cachedOpenSearchHomeFolder = Path.Combine(fs.LocalFolder, cluster.GetCacheFolderName());
+ if (!Directory.Exists(cachedOpenSearchHomeFolder)) return;
+
+ var source = cachedOpenSearchHomeFolder;
+ var target = fs.OpenSearchHome;
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(CopyCachedOpenSearchInstallation)}}} using cached OPENSEARCH_HOME {{{source}}} and copying it to [{target}]");
+ CopyFolder(source, target);
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/CreateLocalApplicationDirectory.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/CreateLocalApplicationDirectory.cs
new file mode 100644
index 0000000000..36f2029592
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/CreateLocalApplicationDirectory.cs
@@ -0,0 +1,51 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System.IO;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.InstallationTasks
+{
+ public class CreateLocalApplicationDirectory : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ var fs = cluster.FileSystem;
+ if (Directory.Exists(fs.LocalFolder))
+ {
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(CreateLocalApplicationDirectory)}}} already exists: {{{fs.LocalFolder}}}");
+ return;
+ }
+
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(CreateLocalApplicationDirectory)}}} creating {{{fs.LocalFolder}}}");
+
+ Directory.CreateDirectory(fs.LocalFolder);
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/DownloadOpenSearchVersion.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/DownloadOpenSearchVersion.cs
new file mode 100644
index 0000000000..3c7d411bb8
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/DownloadOpenSearchVersion.cs
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System.IO;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+using OpenSearch.Stack.ArtifactsApi.Products;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.InstallationTasks
+{
+ public class DownloadOpenSearchVersion : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ if (cluster.CachingAndCachedHomeExists()) return;
+
+ var fs = cluster.FileSystem;
+ var v = cluster.ClusterConfiguration.Version;
+ var a = cluster.ClusterConfiguration.Artifact;
+ var from = v.Artifact(Product.OpenSearch).DownloadUrl;
+ var to = Path.Combine(fs.LocalFolder, a.Archive);
+ if (File.Exists(to))
+ {
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(DownloadOpenSearchVersion)}}} {v} was already downloaded");
+ return;
+ }
+
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(DownloadOpenSearchVersion)}}} downloading OpenSearch [{v}] from {{{from}}} {{{to}}}");
+ DownloadFile(from, to);
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(DownloadOpenSearchVersion)}}} downloaded OpenSearch [{v}] from {{{from}}} {{{to}}}");
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/EnsureJavaHomeEnvironmentVariableIsSet.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/EnsureJavaHomeEnvironmentVariableIsSet.cs
new file mode 100644
index 0000000000..b6f6fd4779
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/EnsureJavaHomeEnvironmentVariableIsSet.cs
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System;
+using System.IO;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.InstallationTasks
+{
+ public class EnsureJavaHomeEnvironmentVariableIsSet : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ var fs = cluster.FileSystem;
+
+ var envVarName = cluster.ClusterConfiguration.JavaHomeEnvironmentVariable;
+ var javaHome = Environment.GetEnvironmentVariable(envVarName);
+ var cachedOpenSearchHomeFolder = Path.Combine(fs.LocalFolder, cluster.GetCacheFolderName());
+ var jdkFolder = Path.Combine(cachedOpenSearchHomeFolder, "jdk");
+ if (Directory.Exists(jdkFolder))
+ {
+ //prefer bundled jdk
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(EnsureJavaHomeEnvironmentVariableIsSet)}}} [{envVarName}] is set to bundled jdk: {{{jdkFolder}}} ");
+ Environment.SetEnvironmentVariable("JAVA_HOME", jdkFolder);
+ }
+ else if (!string.IsNullOrWhiteSpace(javaHome))
+ {
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(EnsureJavaHomeEnvironmentVariableIsSet)}}} [{envVarName}] is set; clearing value for process to prefer bundled jdk...");
+ Environment.SetEnvironmentVariable(envVarName, null);
+ }
+ else
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(EnsureJavaHomeEnvironmentVariableIsSet)}}} {envVarName} is not set proceeding or using default JDK");
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/InitialConfiguration.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/InitialConfiguration.cs
new file mode 100644
index 0000000000..8eb238ed6d
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/InitialConfiguration.cs
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System;
+using System.IO;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+using OpenSearch.Stack.ArtifactsApi;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.InstallationTasks
+{
+ public class InitialConfiguration : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ if (cluster.CachingAndCachedHomeExists()) return;
+
+ if (cluster.ClusterConfiguration.Artifact.ServerType == ServerType.ElasticSearch)
+ {
+ cluster.Writer?.WriteDiagnostic($"{{{nameof(Run)}}} skipping for ElasticSearch");
+ return;
+ }
+
+ var fs = cluster.FileSystem;
+ var script = Path.Combine(fs.OpenSearchHome, "server-initial-config.sh");
+
+ if (cluster.ClusterConfiguration.Artifact.ServerType == ServerType.OpenSearch)
+ File.WriteAllText(script, InitialConfigurationOpenSearch.GetConfigurationScript(cluster.ClusterConfiguration.Version));
+ if (cluster.ClusterConfiguration.Artifact.ServerType == ServerType.OpenDistro)
+ File.WriteAllText(script, InitialConfigurationOpenDistro.GetConfigurationScript());
+
+ cluster.Writer?.WriteDiagnostic($"{{{nameof(Run)}}} going to run [server-initial-config.sh]");
+
+ ExecuteBinary(
+ cluster.ClusterConfiguration,
+ cluster.Writer,
+ "/bin/bash",
+ "run initial cluster configuration",
+ script);
+
+ if (!cluster.ClusterConfiguration.EnableSsl)
+ {
+ if (cluster.ClusterConfiguration.Artifact.ServerType == ServerType.OpenSearch)
+ File.AppendAllText(Path.Combine(fs.OpenSearchHome, "config", "opensearch.yml"), "plugins.security.disabled: true");
+ if (cluster.ClusterConfiguration.Artifact.ServerType == ServerType.OpenDistro)
+ File.AppendAllText(Path.Combine(fs.OpenSearchHome, "config", "elasticsearch.yml"), "opendistro_security.disabled: true");
+ }
+
+ if (cluster.ClusterConfiguration.Artifact.ServerType == ServerType.ElasticSearch && cluster.ClusterConfiguration.EnableSsl)
+ throw new NotImplementedException("ElasticSearch with SSL is not supported");
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/InitialConfigurationOpenDistro.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/InitialConfigurationOpenDistro.cs
new file mode 100644
index 0000000000..cad9b6ca7f
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/InitialConfigurationOpenDistro.cs
@@ -0,0 +1,97 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.InstallationTasks
+{
+ internal class InitialConfigurationOpenDistro
+ {
+ // Actually, it is content of file `opendistro-tar-install.sh` shipped
+ // in the tarball, but due to (1) this file might be changed and
+ // (2) we have to modify the file before execution, because it launches
+ // the server what we want to do on our own, it is decided to have a
+ // snapshot of this file.
+ // The script is taken from v.1.13.3, last 3 lines omitted.
+ public static string GetConfigurationScript() =>
+@"#!/bin/bash
+
+ES_HOME=`dirname $(realpath $0)`; cd $ES_HOME
+ES_KNN_LIB_DIR=$ES_HOME/plugins/opendistro-knn/knn-lib
+##Security Plugin
+bash $ES_HOME/plugins/opendistro_security/tools/install_demo_configuration.sh -y -i -s
+
+##Perf Plugin
+chmod 755 $ES_HOME/plugins/opendistro-performance-analyzer/pa_bin/performance-analyzer-agent
+chmod 755 $ES_HOME/bin/performance-analyzer-agent-cli
+echo ""done security""
+PA_AGENT_JAVA_OPTS=""-Dlog4j.configurationFile=$ES_HOME/plugins/opendistro-performance-analyzer/pa_config/log4j2.xml \
+ -Xms64M -Xmx64M -XX:+UseSerialGC -XX:CICompilerCount=1 -XX:-TieredCompilation -XX:InitialCodeCacheSize=4096 \
+ -XX:InitialBootClassLoaderMetaspaceSize=30720 -XX:MaxRAM=400m""
+
+ES_MAIN_CLASS=""com.amazon.opendistro.elasticsearch.performanceanalyzer.PerformanceAnalyzerApp"" \
+ES_ADDITIONAL_CLASSPATH_DIRECTORIES=plugins/opendistro-performance-analyzer \
+ES_JAVA_OPTS=$PA_AGENT_JAVA_OPTS
+
+if ! grep -q '## OpenDistro Performance Analyzer' $ES_HOME/config/jvm.options; then
+ CLK_TCK=`/usr/bin/getconf CLK_TCK`
+ echo >> $ES_HOME/config/jvm.options
+ echo '## OpenDistro Performance Analyzer' >> $ES_HOME/config/jvm.options
+ echo ""-Dclk.tck=$CLK_TCK"" >> $ES_HOME/config/jvm.options
+ echo ""-Djdk.attach.allowAttachSelf=true"" >> $ES_HOME/config/jvm.options
+ echo ""-Djava.security.policy=$ES_HOME/plugins/opendistro-performance-analyzer/pa_config/es_security.policy"" >> $ES_HOME/config/jvm.options
+fi
+echo ""done plugins""
+
+##Check KNN lib existence in ES TAR distribution
+echo ""Checking kNN library""
+FILE=`ls $ES_KNN_LIB_DIR/libKNNIndex*.so`
+if test -f ""$FILE""; then
+ echo ""FILE EXISTS $FILE""
+else
+ echo ""TEST FAILED OR FILE NOT EXIST $FILE""
+fi
+
+##Set KNN Dylib Path for macOS and *nix systems
+if echo ""$OSTYPE"" | grep -qi ""darwin""; then
+ if echo ""$JAVA_LIBRARY_PATH"" | grep -q ""$ES_KNN_LIB_DIR""; then
+ echo ""KNN lib path has been set""
+ else
+ export JAVA_LIBRARY_PATH=$JAVA_LIBRARY_PATH:$ES_KNN_LIB_DIR
+ echo ""KNN lib path not found, set new path""
+ echo $JAVA_LIBRARY_PATH
+ fi
+else
+ if echo ""$LD_LIBRARY_PATH"" | grep -q ""$ES_KNN_LIB_DIR""; then
+ echo ""KNN lib path has been set""
+ else
+ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$ES_KNN_LIB_DIR
+ echo ""KNN lib path not found, set new path""
+ echo $LD_LIBRARY_PATH
+ fi
+fi
+";
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/InitialConfigurationOpenSearch.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/InitialConfigurationOpenSearch.cs
new file mode 100644
index 0000000000..cda04dac13
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/InitialConfigurationOpenSearch.cs
@@ -0,0 +1,147 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using OpenSearch.Stack.ArtifactsApi;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.InstallationTasks
+{
+ internal class InitialConfigurationOpenSearch
+ {
+ // Actually, it is content of file `opensearch-tar-install.sh` shipped
+ // in the tarball, but due to (1) this file might be changed and
+ // (2) we have to modify the file before execution, because it launches
+ // the server what we want to do on our own, it is decided to have a
+ // snapshot of this file.
+ // The script is taken from v.1.2.4, last 3 lines omitted.
+ public static string GetConfigurationScript(OpenSearchVersion version)
+ {
+ if (version < (OpenSearchVersion)"2.0.0")
+ return
+@"#!/bin/bash
+
+# Copyright OpenSearch Contributors
+# SPDX-License-Identifier: Apache-2.0
+
+OPENSEARCH_HOME=`dirname $(realpath $0)`; cd $OPENSEARCH_HOME
+KNN_LIB_DIR=$OPENSEARCH_HOME/plugins/opensearch-knn/knnlib
+##Security Plugin
+bash $OPENSEARCH_HOME/plugins/opensearch-security/tools/install_demo_configuration.sh -y -i -s
+
+##Perf Plugin
+chmod 755 $OPENSEARCH_HOME/plugins/opensearch-performance-analyzer/pa_bin/performance-analyzer-agent
+chmod 755 $OPENSEARCH_HOME/bin/performance-analyzer-agent-cli
+echo ""done security""
+PA_AGENT_JAVA_OPTS=""-Dlog4j.configurationFile=$OPENSEARCH_HOME/plugins/opensearch-performance-analyzer/pa_config/log4j2.xml \
+ -Xms64M -Xmx64M -XX:+UseSerialGC -XX:CICompilerCount=1 -XX:-TieredCompilation -XX:InitialCodeCacheSize=4096 \
+ -XX:InitialBootClassLoaderMetaspaceSize=30720 -XX:MaxRAM=400m""
+
+OPENSEARCH_MAIN_CLASS=""org.opensearch.performanceanalyzer.PerformanceAnalyzerApp"" \
+OPENSEARCH_ADDITIONAL_CLASSPATH_DIRECTORIES=plugins/opensearch-performance-analyzer \
+OPENSEARCH_JAVA_OPTS=$PA_AGENT_JAVA_OPTS
+
+if ! grep -q '## OpenSearch Performance Analyzer' $OPENSEARCH_HOME/config/jvm.options; then
+ CLK_TCK=`/usr/bin/getconf CLK_TCK`
+ echo >> $OPENSEARCH_HOME/config/jvm.options
+ echo '## OpenSearch Performance Analyzer' >> $OPENSEARCH_HOME/config/jvm.options
+ echo ""-Dclk.tck=$CLK_TCK"" >> $OPENSEARCH_HOME/config/jvm.options
+ echo ""-Djdk.attach.allowAttachSelf=true"" >> $OPENSEARCH_HOME/config/jvm.options
+ echo ""-Djava.security.policy=$OPENSEARCH_HOME/plugins/opensearch-performance-analyzer/pa_config/opensearch_security.policy"" >> $OPENSEARCH_HOME/config/jvm.options
+fi
+echo ""done plugins""
+
+##Set KNN Dylib Path for macOS and *nix systems
+if echo ""$OSTYPE"" | grep -qi ""darwin""; then
+ if echo ""$JAVA_LIBRARY_PATH"" | grep -q ""$KNN_LIB_DIR""; then
+ echo ""k-NN libraries found in JAVA_LIBRARY_PATH""
+ else
+ export JAVA_LIBRARY_PATH=$JAVA_LIBRARY_PATH:$KNN_LIB_DIR
+ echo ""k-NN libraries not found in JAVA_LIBRARY_PATH. Updating path to: $JAVA_LIBRARY_PATH.""
+ fi
+else
+ if echo ""$LD_LIBRARY_PATH"" | grep -q ""$KNN_LIB_DIR""; then
+ echo ""k-NN libraries found in LD_LIBRARY_PATH""
+ else
+ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$KNN_LIB_DIR
+ echo ""k-NN libraries not found in LD_LIBRARY_PATH. Updating path to: $LD_LIBRARY_PATH.""
+ fi
+fi
+";
+ return
+ //script from 2.0.0
+ @"#!/bin/bash
+
+# Copyright OpenSearch Contributors
+# SPDX-License-Identifier: Apache-2.0
+
+export OPENSEARCH_HOME=`dirname $(realpath $0)`
+export OPENSEARCH_PATH_CONF=$OPENSEARCH_HOME/config
+cd $OPENSEARCH_HOME
+
+KNN_LIB_DIR=$OPENSEARCH_HOME/plugins/opensearch-knn/lib
+##Security Plugin
+bash $OPENSEARCH_HOME/plugins/opensearch-security/tools/install_demo_configuration.sh -y -i -s
+
+echo ""done security""
+PA_AGENT_JAVA_OPTS=""-Dlog4j.configurationFile=$OPENSEARCH_PATH_CONF/opensearch-performance-analyzer/log4j2.xml \
+ -Xms64M -Xmx64M -XX:+UseSerialGC -XX:CICompilerCount=1 -XX:-TieredCompilation -XX:InitialCodeCacheSize=4096 \
+ -XX:MaxRAM=400m""
+
+OPENSEARCH_MAIN_CLASS=""org.opensearch.performanceanalyzer.PerformanceAnalyzerApp"" \
+OPENSEARCH_ADDITIONAL_CLASSPATH_DIRECTORIES=plugins/opensearch-performance-analyzer \
+OPENSEARCH_JAVA_OPTS=$PA_AGENT_JAVA_OPTS
+
+if ! grep -q '## OpenSearch Performance Analyzer' $OPENSEARCH_PATH_CONF/jvm.options; then
+ CLK_TCK=`/usr/bin/getconf CLK_TCK`
+ echo >> $OPENSEARCH_PATH_CONF/jvm.options
+ echo '## OpenSearch Performance Analyzer' >> $OPENSEARCH_PATH_CONF/jvm.options
+ echo ""-Dclk.tck=$CLK_TCK"" >> $OPENSEARCH_PATH_CONF/jvm.options
+ echo ""-Djdk.attach.allowAttachSelf=true"" >> $OPENSEARCH_PATH_CONF/jvm.options
+ echo ""-Djava.security.policy=$OPENSEARCH_PATH_CONF/opensearch-performance-analyzer/opensearch_security.policy"" >> $OPENSEARCH_PATH_CONF/jvm.options
+ echo ""--add-opens=jdk.attach/sun.tools.attach=ALL-UNNAMED"" >> $OPENSEARCH_PATH_CONF/jvm.options
+fi
+echo ""done plugins""
+
+##Set KNN Dylib Path for macOS and *nix systems
+if echo ""$OSTYPE"" | grep -qi ""darwin""; then
+ if echo ""$JAVA_LIBRARY_PATH"" | grep -q ""$KNN_LIB_DIR""; then
+ echo ""k-NN libraries found in JAVA_LIBRARY_PATH""
+ else
+ export JAVA_LIBRARY_PATH=$JAVA_LIBRARY_PATH:$KNN_LIB_DIR
+ echo ""k-NN libraries not found in JAVA_LIBRARY_PATH. Updating path to: $JAVA_LIBRARY_PATH.""
+ fi
+else
+ if echo ""$LD_LIBRARY_PATH"" | grep -q ""$KNN_LIB_DIR""; then
+ echo ""k-NN libraries found in LD_LIBRARY_PATH""
+ else
+ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$KNN_LIB_DIR
+ echo ""k-NN libraries not found in LD_LIBRARY_PATH. Updating path to: $LD_LIBRARY_PATH.""
+ fi
+fi
+";
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/InstallPlugins.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/InstallPlugins.cs
new file mode 100644
index 0000000000..b8de7d94bf
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/InstallPlugins.cs
@@ -0,0 +1,135 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System;
+using System.IO;
+using System.Linq;
+using OpenSearch.OpenSearch.Managed;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+using OpenSearch.OpenSearch.Managed.FileSystem;
+using OpenSearch.Stack.ArtifactsApi;
+using OpenSearch.Stack.ArtifactsApi.Products;
+using ProcNet.Std;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.InstallationTasks
+{
+ public class InstallPlugins : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ if (cluster.CachingAndCachedHomeExists()) return;
+
+ var v = cluster.ClusterConfiguration.Version;
+
+ var fs = cluster.FileSystem;
+ var requiredPlugins = cluster.ClusterConfiguration.Plugins;
+
+ if (cluster.ClusterConfiguration.ValidatePluginsToInstall)
+ {
+ var invalidPlugins = requiredPlugins
+ .Where(p => !p.IsValid(v))
+ .Select(p => p.SubProductName).ToList();
+ if (invalidPlugins.Any())
+ throw new OpenSearchCleanExitException(
+ $"Can not install the following plugins for version {v}: {string.Join(", ", invalidPlugins)} ");
+ }
+
+ foreach (var plugin in requiredPlugins)
+ {
+ var includedByDefault = plugin.IsIncludedOutOfTheBox(v);
+ if (includedByDefault)
+ {
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(Run)}}} SKIP plugin [{plugin.SubProductName}] shipped OOTB as of: {{{plugin.ShippedByDefaultAsOf}}}");
+ continue;
+ }
+
+ var validForCurrentVersion = plugin.IsValid(v);
+ if (!validForCurrentVersion)
+ {
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(Run)}}} SKIP plugin [{plugin.SubProductName}] not valid for version: {{{v}}}");
+ continue;
+ }
+
+ var alreadyInstalled = AlreadyInstalled(fs, plugin.SubProductName);
+ if (alreadyInstalled)
+ {
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(Run)}}} SKIP plugin [{plugin.SubProductName}] already installed");
+ continue;
+ }
+
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(Run)}}} attempting install [{plugin.SubProductName}] as it's not OOTB: {{{plugin.ShippedByDefaultAsOf}}} and valid for {v}: {{{plugin.IsValid(v)}}}");
+
+ if (!Directory.Exists(fs.ConfigPath)) Directory.CreateDirectory(fs.ConfigPath);
+ ExecuteBinary(
+ cluster.ClusterConfiguration,
+ cluster.Writer,
+ fs.PluginBinary,
+ $"install opensearch plugin: {plugin.SubProductName}",
+ "install --batch", GetPluginLocation(plugin, v));
+
+ CopyConfigDirectoryToHomeCacheConfigDirectory(cluster, plugin);
+ }
+ }
+
+ private static string GetPluginLocation(OpenSearchPlugin plugin, OpenSearchVersion v)
+ {
+ // OpenSearch 1.0.0 artifacts were not published. The plugins are built in the workflow and used here.
+ if (v == "1.0.0")
+ // The environment variable is set in the integration workflow in
+ // https://github.com/opensearch-project/opensearch-net/blob/main/.github/workflows/integration.yml
+ return "file://" + Environment.GetEnvironmentVariable("plugins-directory") + $"/{plugin.SubProductName}-{v}.zip";
+ else
+ return plugin.SubProductName;
+ }
+
+ private static void CopyConfigDirectoryToHomeCacheConfigDirectory(
+ IEphemeralCluster cluster, OpenSearchPlugin plugin)
+ {
+ if (!cluster.ClusterConfiguration.CacheOpenSearchHomeInstallation) return;
+ var fs = cluster.FileSystem;
+ var cachedOpenSearchHomeFolder = Path.Combine(fs.LocalFolder, cluster.GetCacheFolderName());
+ var configTarget = Path.Combine(cachedOpenSearchHomeFolder, "config");
+
+ var configPluginPath = Path.Combine(fs.ConfigPath, plugin.SubProductName);
+ var configPluginPathCached = Path.Combine(configTarget, plugin.SubProductName);
+ if (!Directory.Exists(configPluginPath) || Directory.Exists(configPluginPathCached)) return;
+
+ Directory.CreateDirectory(configPluginPathCached);
+ CopyFolder(configPluginPath, configPluginPathCached);
+ }
+
+ private static bool AlreadyInstalled(INodeFileSystem fileSystem, string folderName)
+ {
+ var pluginFolder = Path.Combine(fileSystem.OpenSearchHome, "plugins", folderName);
+ return Directory.Exists(pluginFolder);
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/PrintConfiguration.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/PrintConfiguration.cs
new file mode 100644
index 0000000000..7a4caee531
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/PrintConfiguration.cs
@@ -0,0 +1,75 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System;
+using System.Linq;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+using static OpenSearch.OpenSearch.Ephemeral.ClusterFeatures;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.InstallationTasks
+{
+ public class PrintConfiguration : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ var c = cluster.ClusterConfiguration;
+ var version = c.Version;
+
+ string F(ClusterFeatures feature)
+ {
+ return c.Features.HasFlag(feature) ? Enum.GetName(typeof(ClusterFeatures), feature) : string.Empty;
+ }
+
+ var features = string.Join("|",
+ new[] { F(SSL)}.Where(v => !string.IsNullOrWhiteSpace(v)));
+ features = string.IsNullOrWhiteSpace(features) ? "None" : features;
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(PrintConfiguration)}}} starting {{{version}}} with features [{features}]");
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(PrintConfiguration)}}} {{{nameof(c.NumberOfNodes)}}} [{c.NumberOfNodes}]");
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(PrintConfiguration)}}} {{{nameof(c.ClusterName)}}} [{c.ClusterName}]");
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(PrintConfiguration)}}} {{{nameof(c.EnableSsl)}}} [{c.EnableSsl}]");
+ cluster.Writer?.WriteDiagnostic($"{{{nameof(PrintConfiguration)}}} {{{nameof(c.Plugins)}}} [{c.Plugins}]");
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(PrintConfiguration)}}} {{{nameof(c.CacheOpenSearchHomeInstallation)}}} [{c.CacheOpenSearchHomeInstallation}]");
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(PrintConfiguration)}}} {{{nameof(c.ShowOpenSearchOutputAfterStarted)}}} [{c.ShowOpenSearchOutputAfterStarted}]");
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(PrintConfiguration)}}} {{{nameof(c.ValidatePluginsToInstall)}}} [{c.ValidatePluginsToInstall}]");
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(PrintConfiguration)}}} {{{nameof(c.PrintYamlFilesInConfigFolder)}}} [{c.PrintYamlFilesInConfigFolder}]");
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(PrintConfiguration)}}} {{{nameof(c.SkipBuiltInAfterStartTasks)}}} [{c.SkipBuiltInAfterStartTasks}]");
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(PrintConfiguration)}}} {{{nameof(c.HttpFiddlerAware)}}} [{c.HttpFiddlerAware}]");
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(PrintConfiguration)}}} {{{nameof(c.NoCleanupAfterNodeStopped)}}} [{c.NoCleanupAfterNodeStopped}]");
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/SetOpenSearchBundledJdkJavaHome.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/SetOpenSearchBundledJdkJavaHome.cs
new file mode 100644
index 0000000000..673dea2801
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/SetOpenSearchBundledJdkJavaHome.cs
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System;
+using System.IO;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.InstallationTasks
+{
+ public class SetOpenSearchBundledJdkJavaHome : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ var fs = cluster.FileSystem;
+ var jdkFolder = Path.Combine(fs.OpenSearchHome, "jdk");
+ if (Directory.Exists(jdkFolder))
+ {
+ var envVarName = cluster.ClusterConfiguration.JavaHomeEnvironmentVariable;
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(SetOpenSearchBundledJdkJavaHome)}}} [{envVarName}] is set to bundled jdk: {{{jdkFolder}}} ");
+ Environment.SetEnvironmentVariable(envVarName, jdkFolder);
+ }
+ else
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(SetOpenSearchBundledJdkJavaHome)}}} [No bundled jdk found] looked in: {{{jdkFolder}}} ");
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/UnzipOpenSearch.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/UnzipOpenSearch.cs
new file mode 100644
index 0000000000..8854d061b9
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/InstallationTasks/UnzipOpenSearch.cs
@@ -0,0 +1,67 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System.IO;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.InstallationTasks
+{
+ public class UnzipOpenSearch : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ if (cluster.CachingAndCachedHomeExists()) return;
+
+ var fs = cluster.FileSystem;
+ var v = cluster.ClusterConfiguration.Version;
+ var a = cluster.ClusterConfiguration.Artifact;
+ if (Directory.Exists(fs.OpenSearchHome))
+ {
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(UnzipOpenSearch)}}} skipping [{fs.OpenSearchHome}] already exists");
+ return;
+ }
+
+ var from = Path.Combine(fs.LocalFolder, a.Archive);
+ var extractedFolder = Path.Combine(fs.LocalFolder, a.FolderInZip);
+ if (!Directory.Exists(extractedFolder))
+ {
+ cluster.Writer?.WriteDiagnostic($"{{{nameof(UnzipOpenSearch)}}} unzipping version [{v}] {{{from}}}");
+ Extract(from, fs.LocalFolder);
+
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(UnzipOpenSearch)}}} extracted version [{v}] to {{{fs.LocalFolder}}}");
+ }
+
+ if (extractedFolder == fs.OpenSearchHome) return;
+
+ cluster.Writer?.WriteDiagnostic(
+ $"{{{nameof(UnzipOpenSearch)}}} Copying extracted folder {{{extractedFolder}}} => {fs.OpenSearchHome}");
+ CopyFolder(extractedFolder, fs.OpenSearchHome);
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/ValidationTasks/ValidateClusterStateTask.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/ValidationTasks/ValidateClusterStateTask.cs
new file mode 100644
index 0000000000..6b98b880bc
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/ValidationTasks/ValidateClusterStateTask.cs
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System;
+using System.Net;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.ValidationTasks
+{
+ public class ValidateClusterStateTask : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ cluster.Writer.WriteDiagnostic(
+ $"{{{nameof(ValidateClusterStateTask)}}} waiting cluster to go into yellow health state");
+ var healthyResponse = Get(cluster, "_cluster/health", "wait_for_status=yellow&timeout=20s");
+ if (healthyResponse == null || healthyResponse.StatusCode != HttpStatusCode.OK)
+ throw new Exception(
+ $"Cluster health waiting for status yellow failed after 20s {GetResponseException(healthyResponse)}");
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/ValidationTasks/ValidatePluginsTask.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/ValidationTasks/ValidatePluginsTask.cs
new file mode 100644
index 0000000000..20207f7450
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/ValidationTasks/ValidatePluginsTask.cs
@@ -0,0 +1,67 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System;
+using System.Linq;
+using OpenSearch.OpenSearch.Ephemeral.Tasks.InstallationTasks;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+using OpenSearch.Stack.ArtifactsApi;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.ValidationTasks
+{
+ public class ValidatePluginsTask : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ var v = cluster.ClusterConfiguration.Version;
+ var requestPlugins = cluster.ClusterConfiguration.Plugins
+ .Where(p => p.IsValid(v))
+ .Where(p => !p.IsIncludedOutOfTheBox(v))
+ .Select(p => p.GetExistsMoniker(v))
+ .ToList();
+ if (!requestPlugins.Any()) return;
+
+ cluster.Writer.WriteDiagnostic(
+ $"{{{nameof(ValidatePluginsTask)}}} validating the cluster is running the requested plugins");
+ var catPlugins = Get(cluster, "_cat/plugins", "h=component");
+ if (catPlugins == null || !catPlugins.IsSuccessStatusCode)
+ throw new Exception(
+ $"Calling _cat/plugins did not result in an OK response {GetResponseException(catPlugins)}");
+
+ var installedPlugins = GetResponseString(catPlugins)
+ .Split(new[] {'\n'}, StringSplitOptions.RemoveEmptyEntries).ToList();
+
+ var missingPlugins = requestPlugins.Except(installedPlugins).ToList();
+ if (!missingPlugins.Any()) return;
+
+ var missingString = string.Join(", ", missingPlugins);
+ var pluginsString = string.Join(", ", installedPlugins);
+ throw new Exception(
+ $"Already running opensearch missed the following plugin(s): {missingString} currently installed: {pluginsString}.");
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/ValidationTasks/ValidateRunningVersion.cs b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/ValidationTasks/ValidateRunningVersion.cs
new file mode 100644
index 0000000000..f0f2da1377
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.Ephemeral/Tasks/ValidationTasks/ValidateRunningVersion.cs
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using OpenSearch.OpenSearch.Managed.ConsoleWriters;
+using OpenSearch.Stack.ArtifactsApi;
+
+namespace OpenSearch.OpenSearch.Ephemeral.Tasks.ValidationTasks
+{
+ public class ValidateRunningVersion : ClusterComposeTask
+ {
+ public override void Run(IEphemeralCluster cluster)
+ {
+ void WriteDiagnostic(string message) =>
+ cluster.Writer?.WriteDiagnostic($"{{{nameof(ValidateRunningVersion)}}} {message}");
+ var requestedVersion = cluster.ClusterConfiguration.Version;
+ if (cluster.ClusterConfiguration.Artifact.ServerType == ServerType.OpenDistro)
+ //All supported version of OpenDistro are based on ElasticSearch v.7.10.2
+ requestedVersion = OpenSearchVersion.From("7.10.2");
+
+
+ WriteDiagnostic($"validating the cluster is running the requested version: {requestedVersion}");
+
+ HttpResponseMessage catNodes = null;
+ var retryCount = 4;
+ var initialRetryWait = 5;
+ foreach (var retry in Enumerable.Range(1, retryCount))
+ {
+ catNodes = Get(cluster, "_cat/nodes", "h=version");
+ if (catNodes.IsSuccessStatusCode) break;
+ var retryWait = TimeSpan.FromSeconds(initialRetryWait * retry);
+ WriteDiagnostic($"{catNodes.StatusCode} response for GET _cat/nodes. Waiting to retry #{retry}");
+ Thread.Sleep(retryWait);
+ }
+ if (catNodes is not {IsSuccessStatusCode: true})
+ {
+ throw new Exception(
+ $"Calling _cat/nodes for version checking did not result in an OK response {GetResponseException(catNodes)}");
+ }
+
+ var nodeVersions = GetResponseString(catNodes).Split(new[] {'\n'}, StringSplitOptions.RemoveEmptyEntries)
+ .ToList();
+
+ var anchorVersion = $"{requestedVersion.Major}.{requestedVersion.Minor}.{requestedVersion.Patch}";
+ var allOnRequestedVersion = nodeVersions.All(v => v.Trim() == anchorVersion);
+ if (!allOnRequestedVersion)
+ {
+ throw new Exception(
+ $"Not all the running nodes in the cluster are on requested version: {anchorVersion} received: {string.Join(", ", nodeVersions)}");
+ }
+ }
+ }
+}
diff --git a/abstractions/src/OpenSearch.OpenSearch.EphemeralTests/EphemeralClusterTests.cs b/abstractions/src/OpenSearch.OpenSearch.EphemeralTests/EphemeralClusterTests.cs
new file mode 100644
index 0000000000..1e9e88b450
--- /dev/null
+++ b/abstractions/src/OpenSearch.OpenSearch.EphemeralTests/EphemeralClusterTests.cs
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: Apache-2.0
+*
+* The OpenSearch Contributors require contributions made to
+* this file be licensed under the Apache-2.0 license or a
+* compatible open source license.
+*
+* Modifications Copyright OpenSearch Contributors. See
+* GitHub history for details.
+*
+* Licensed to Elasticsearch B.V. under one or more contributor
+* license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright
+* ownership. Elasticsearch B.V. licenses this file to you under
+* the Apache License, Version 2.0 (the "License"); you may
+* not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+using System.Collections;
+using System.Collections.Generic;
+using OpenSearch.OpenSearch.Ephemeral;
+using OpenSearch.Stack.ArtifactsApi;
+using Xunit;
+
+namespace OpenSearch.OpenSearch.EphemeralTests
+{
+ public class EphemeralClusterTests
+ {
+ [Theory]
+ [ClassData(typeof(SampleClusters))]
+ public void TestEphemeralCluster(OpenSearchVersion version, ServerType serverType, ClusterFeatures features)
+ {
+ var numberOfNodes = 1;
+ var clusterConfiguration =
+ new EphemeralClusterConfiguration(version, serverType, features, null, numberOfNodes)
+ {
+ ShowOpenSearchOutputAfterStarted = true
+ };
+ using var cluster = new EphemeralCluster(clusterConfiguration);
+ var timeout = new System.TimeSpan(0, 5, 30);
+ using (cluster.Start(timeout))
+ {
+ Assert.True(cluster.Started, "OpenSearch cluster started");
+ foreach (var n in cluster.Nodes)
+ {
+ n.SendControlC();
+ Assert.True(n.WaitForCompletion(timeout), $"Failed to stop node {n.ProcessId}");
+ }
+ }
+ }
+
+ private class SampleClusters : IEnumerable