diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..1326700af
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,3 @@
+{
+ "image": "mcr.microsoft.com/dotnet/sdk:8.0"
+}
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
index 9345e1b0b..9a3d9c40d 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -44,4 +44,21 @@ csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
-csharp_new_line_before_members_in_anonymous_types = true
\ No newline at end of file
+csharp_new_line_before_members_in_anonymous_types = true
+
+# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/unnecessary-code-rules
+
+# Avoid unused private fields
+dotnet_diagnostic.CA1823.severity = error
+
+# Use string.Contains(char) instead of string.Contains(string) with single characters
+dotnet_diagnostic.CA1847.severity = error
+
+# Remove unnecessary import
+dotnet_diagnostic.IDE0005.severity = error
+
+# Private member is unused
+dotnet_diagnostic.IDE0051.severity = error
+
+# Private member is unread
+dotnet_diagnostic.IDE0052.severity = error
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..65f0cf8c6
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,52 @@
+*.doc diff=astextplain
+*.DOC diff=astextplain
+*.docx diff=astextplain
+*.DOCX diff=astextplain
+*.dot diff=astextplain
+*.DOT diff=astextplain
+*.pdf diff=astextplain
+*.PDF diff=astextplain
+*.rtf diff=astextplain
+*.RTF diff=astextplain
+
+*.jpg binary
+*.png binary
+*.gif binary
+
+*.cs text=auto diff=csharp
+*.vb text=auto
+*.resx text=auto
+*.c text=auto
+*.cpp text=auto
+*.cxx text=auto
+*.h text=auto
+*.hxx text=auto
+*.py text=auto
+*.rb text=auto
+*.java text=auto
+*.html text=auto
+*.htm text=auto
+*.css text=auto
+*.scss text=auto
+*.sass text=auto
+*.less text=auto
+*.js text=auto
+*.lisp text=auto
+*.clj text=auto
+*.sql text=auto
+*.php text=auto
+*.lua text=auto
+*.m text=auto
+*.asm text=auto
+*.erl text=auto
+*.fs text=auto
+*.fsx text=auto
+*.hs text=auto
+
+*.csproj text=auto
+*.vbproj text=auto
+*.fsproj text=auto
+*.dbproj text=auto
+*.sln text=auto eol=crlf
+
+src/KubernetesClient/generated/** linguist-generated
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..841fcae61
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,37 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**Kubernetes C# SDK Client Version**
+e.g. `9.0.1`
+
+**Server Kubernetes Version**
+e.g. `1.22.3`
+
+**Dotnet Runtime Version**
+e.g. net6
+
+**To Reproduce**
+Steps to reproduce the behavior:
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**KubeConfig**
+If applicable, add a KubeConfig file with secrets redacted.
+
+**Where do you run your app with Kubernetes SDK (please complete the following information):**
+ - OS: [e.g. Linux]
+ - Environment [e.g. container]
+ - Cloud [e.g. Azure]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..2fb1795e4
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,31 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+
+ # Maintain dependencies for GitHub Actions
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ # Allow up to 5 open pull requests for GitHub Actions dependencies
+ open-pull-requests-limit: 5
+ labels:
+ - "dependencies"
+ # Rebase open pull requests when changes are detected
+ rebase-strategy: "auto"
+
+ # Maintain dependencies for NuGet packages
+ - package-ecosystem: "nuget"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ # Allow up to 10 open pull requests for NuGet dependencies
+ open-pull-requests-limit: 10
+ labels:
+ - "dependencies"
+ # Rebase open pull requests when changes are detected
+ rebase-strategy: "auto"
diff --git a/.github/workflows/buildtest.yaml b/.github/workflows/buildtest.yaml
new file mode 100644
index 000000000..26d585ec7
--- /dev/null
+++ b/.github/workflows/buildtest.yaml
@@ -0,0 +1,90 @@
+name: Build and Test
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest, macOS-latest]
+ name: Dotnet build
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: |
+ 8.0.x
+ 9.0.x
+ - name: Build
+ run: dotnet build --configuration Release
+ - name: Test
+ run: dotnet test --configuration Release --collect:"Code Coverage;Format=Cobertura" --logger trx --results-directory TestResults --settings CodeCoverage.runsettings --no-build
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ directory: ./TestResults
+ files: '*.cobertura.xml'
+ - name: Upload test results
+ uses: actions/upload-artifact@v5
+ with:
+ name: test-results-${{ matrix.os }}
+ path: ./TestResults
+ if: ${{ always() }} # Always run this step even on failure
+
+ # Test code gen for visual studio compatibility >> https://github.com/kubernetes-client/csharp/pull/1008
+ codgen:
+ runs-on: windows-latest
+ name: MSBuild build
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - name: Add msbuild to PATH
+ uses: microsoft/setup-msbuild@v2
+ - name: Setup dotnet SDK
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: '9.0.x'
+ - name: Restore nugets (msbuild)
+ run: msbuild .\src\KubernetesClient\ -t:restore -p:RestorePackagesConfig=true
+ - name: Build (msbuild)
+ run: msbuild .\src\KubernetesClient\
+
+ e2e:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: |
+ 8.0.x
+ 9.0.x
+ - name: Minikube
+ run: minikube start
+ - name: Test
+ run: |
+ true > skip.log
+ env K8S_E2E_MINIKUBE=1 dotnet test tests/E2E.Tests --logger "SkipTestLogger;file=$PWD/skip.log" -p:BuildInParallel=false
+ if [ -s skip.log ]; then
+ cat skip.log
+ echo "CASES MUST NOT BE SKIPPED"
+ exit 1
+ fi
+ - name: AOT Test
+ run: |
+ true > skip.log
+ env K8S_E2E_MINIKUBE=1 dotnet test tests/E2E.Aot.Tests --logger "SkipTestLogger;file=$PWD/skip.log" -p:BuildInParallel=false
+ if [ -s skip.log ]; then
+ cat skip.log
+ echo "CASES MUST NOT BE SKIPPED"
+ exit 1
+ fi
+
+on:
+ pull_request:
+ types: [assigned, opened, synchronize, reopened]
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 000000000..6355396eb
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,62 @@
+name: "CodeQL"
+
+permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ master ]
+ schedule:
+ - cron: '15 23 * * 1'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: windows-2022
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'csharp' ]
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: |
+ 8.0.x
+ 9.0.x
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v4
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ # Currently .NET8.0 isn't supported
+ # - name: Autobuild
+ # uses: github/codeql-action/autobuild@v2
+
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --configuration Debug --no-restore
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v4
diff --git a/.github/workflows/docfx.yaml b/.github/workflows/docfx.yaml
new file mode 100644
index 000000000..3eec06ec3
--- /dev/null
+++ b/.github/workflows/docfx.yaml
@@ -0,0 +1,56 @@
+name: Docfx
+
+on:
+ push:
+ branches: [ master ]
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow one concurrent deployment
+concurrency:
+ group: "pages"
+ cancel-in-progress: true
+
+jobs:
+ docfx:
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: |
+ 8.0.x
+ 9.0.x
+
+ - name: Build
+ run: dotnet build -c Release
+
+ - uses: nunit/docfx-action@v4.1.0
+ name: Build Documentation
+ with:
+ args: doc/docfx.json
+
+ - name: Setup Pages
+ uses: actions/configure-pages@v5
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v4
+ with:
+ # Upload entire repository
+ path: doc/_site
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/draft.yaml b/.github/workflows/draft.yaml
new file mode 100644
index 000000000..01b098518
--- /dev/null
+++ b/.github/workflows/draft.yaml
@@ -0,0 +1,42 @@
+name: Draft Release
+
+permissions:
+ contents: write
+
+on:
+ push:
+ branches: [ master ]
+
+jobs:
+ draft:
+
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: |
+ 8.0.x
+ 9.0.x
+
+ - name: dotnet restore
+ run: dotnet restore --verbosity minimal --configfile nuget.config
+
+ - name: dotnet test
+ run: dotnet test
+
+ - uses: dotnet/nbgv@master
+ with:
+ setAllVars: true
+
+ - name: create release
+ shell: pwsh
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ gh release create -d --generate-notes v$env:NBGV_NuGetPackageVersion
diff --git a/.github/workflows/nuget.yaml b/.github/workflows/nuget.yaml
new file mode 100644
index 000000000..fa654822f
--- /dev/null
+++ b/.github/workflows/nuget.yaml
@@ -0,0 +1,60 @@
+name: Nuget
+
+on:
+ release:
+ types: [ released ]
+
+jobs:
+ nuget:
+
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: |
+ 8.0.x
+ 9.0.x
+
+ - name: dotnet restore
+ run: dotnet restore --verbosity minimal --configfile nuget.config
+
+ - name: dotnet test
+ run: dotnet test
+
+ - name: dotnet pack
+ run: dotnet pack -c Release src/nuget.proj -o pkg --include-symbols
+
+ - name: dotnet nuget push
+ run: |
+ dotnet nuget push pkg\*.nupkg -s https://nuget.pkg.github.com/$env:GITHUB_REPOSITORY_OWNER -k ${{ secrets.GITHUB_TOKEN }} --skip-duplicate
+ dotnet nuget push pkg\*.nupkg -s https://www.nuget.org/ -k ${{ secrets.nuget_api_key }} --skip-duplicate
+
+
+ ## Remove old versions of NuGet packages form github NuGet feed
+ nuget-delete-old-packages:
+ name: "Delete Old NuGet"
+ needs: [nuget]
+ strategy:
+ matrix:
+ nuget-package:
+ - "KubernetesClient"
+ - "KubernetesClient.Classic"
+ runs-on: ubuntu-latest
+ permissions:
+ packages: write
+
+ steps:
+ - name: Delete old NuGet packages
+ uses: actions/delete-package-versions@v5
+ with:
+ owner: ${{ env.GITHUB_REPOSITORY_OWNER }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+ package-name: ${{ matrix.nuget-package }}
+ package-type: nuget
+ min-versions-to-keep: 10
diff --git a/.gitignore b/.gitignore
index 201f8578f..46bc886d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,9 +2,19 @@
.vs
obj/
bin/
+**/TestResults
# User-specific VS files
*.suo
*.user
*.userosscache
*.sln.docstates
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+launchSettings.json
+*.DotSettings
+
+*.sln
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 6051ec73a..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-language: csharp
-sudo: false
-matrix:
- include:
- - dotnet: 2.0.0
- mono: none
- dist: trusty
-
-script:
- - ./ci.sh
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..bac9ba9ab
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,22 @@
+# Contributing
+
+Thanks for taking the time to join our community and start contributing!
+
+Please remember to read and observe the [Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md).
+
+This project accepts contribution via github [pull requests](https://help.github.com/articles/about-pull-requests/). This document outlines the process to help get your contribution accepted. Please also read the [Kubernetes contributor guide](https://github.com/kubernetes/community/blob/master/contributors/guide/README.md) which provides detailed instructions on how to get your ideas and bug fixes seen and accepted.
+
+## Sign the Contributor License Agreement
+We'd love to accept your patches! Before we can accept them you need to sign Cloud Native Computing Foundation (CNCF) [CLA](https://github.com/kubernetes/community/blob/master/CLA.md).
+
+## Reporting an issue
+If you have any problem with the package or any suggestions, please file an [issue](https://github.com/kubernetes-client/csharp/issues).
+
+## Contributing a Patch
+1. Submit an issue describing your proposed change to the repo.
+2. Fork this repo, develop and test your code changes.
+3. Submit a pull request.
+4. The bot will automatically assigns someone to review your PR. Check the full list of bot commands [here](https://prow.k8s.io/command-help).
+
+### Contact
+You can reach the maintainers of this project at [SIG API Machinery](https://github.com/kubernetes/community/tree/master/sig-api-machinery) or on the [#kubernetes-client](https://kubernetes.slack.com/messages/kubernetes-client) channel on the Kubernetes slack.
diff --git a/CodeCoverage.runsettings b/CodeCoverage.runsettings
new file mode 100644
index 000000000..acc025c10
--- /dev/null
+++ b/CodeCoverage.runsettings
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+ .*KubernetesClient\..*\.dll$
+
+
+ .*tests\.dll$
+ .*xunit.*dll$
+ .*moq\.dll$
+ .*System\.Reactive\.dll$
+ .*BouncyCastle\.Crypto\.dll$
+ .*IdentityModel\.OidcClient\.dll$
+
+
+
+ True
+
+ True
+
+ True
+
+
+ ^System.ObsoleteAttribute$
+ ^System.CodeDom.Compiler.GeneratedCodeAttribute$
+ ^System.Diagnostics.DebuggerHiddenAttribute$
+ ^System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute$
+
+
+
+
+
+
+
+
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 000000000..2a5ed0116
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,41 @@
+
+
+
+ $(MSBuildThisFileDirectory)\kubernetes-client.ruleset
+ true
+ true
+
+
+
+ The Kubernetes Project Authors
+ 2017 The Kubernetes Project Authors
+ Client library for the Kubernetes open source container orchestrator.
+
+ Apache-2.0
+ https://github.com/kubernetes-client/csharp
+ https://raw.githubusercontent.com/kubernetes/kubernetes/master/logo/logo.png
+ logo.png
+ kubernetes;docker;containers;
+ true
+
+
+ true
+
+
+ true
+ snupkg
+ true
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+ true
+ portable
+ 13.0
+
+
+
+ true
+
+
+
+
+
+
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 000000000..517121e49
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 000000000..27783a77c
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,54 @@
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 8dada3eda..90fe7c792 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright {yyyy} {name of copyright owner}
+ Copyright 2017 the Kubernetes Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/OWNERS b/OWNERS
index 74eace9c9..ef8b7dd29 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,7 +1,10 @@
+# See the OWNERS docs at https://go.k8s.io/owners
+
approvers:
- brendandburns
-- krabhishek8260
-reviewer:
+- tg123
+reviewers:
- brendandburns
-- krabhishek8260
-
+- tg123
+emeritus_approvers:
+- krabhishek8260 # 4/4/2022
diff --git a/README.md b/README.md
index e214a5875..c8eb91626 100644
--- a/README.md
+++ b/README.md
@@ -1,44 +1,90 @@
# Kubernetes C# Client
-[](https://travis-ci.org/kubernetes-client/csharp)
+
+[](https://github.com/kubernetes-client/csharp/actions/workflows/buildtest.yaml)
[](http://bit.ly/kubernetes-client-capabilities-badge)
[](http://bit.ly/kubernetes-client-support-badge)
# Usage
-[Nuget Package](https://www.nuget.org/packages/KubernetesClient/)
+
+[](https://www.nuget.org/packages/KubernetesClient/)
```sh
dotnet add package KubernetesClient
```
-# Generating the Client Code
+## Generate with Visual Studio
-## Prerequisites
+```
+dotnet msbuild /Restore /t:SlnGen kubernetes-client.proj
+```
-Check out the generator project into some other directory
-(henceforth `$GEN_DIR`)
+## Authentication/Configuration
+You should be able to use a standard KubeConfig file with this library,
+see the `BuildConfigFromConfigFile` function below. Most authentication
+methods are currently supported, but a few are not, see the
+[known-issues](https://github.com/kubernetes-client/csharp#known-issues).
-```bash
-cd $GEN_DIR/..
-git clone https://github.com/kubernetes-client/gen
-```
+You should also be able to authenticate with the in-cluster service
+account using the `InClusterConfig` function shown below.
-Install the [`autorest` tool](https://github.com/azure/autorest):
+## Monitoring
+Metrics are built in to HttpClient using System.Diagnostics.DiagnosticsSource.
+https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics-system-net
-```bash
-npm install autorest
+There are many ways these metrics can be consumed/exposed but that decision is up to the application, not KubernetesClient itself.
+https://learn.microsoft.com/en-us/dotnet/core/diagnostics/metrics-collection
+
+## Sample Code
+
+### Creating the client
+```c#
+// Load from the default kubeconfig on the machine.
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+
+// Load from a specific file:
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile(Environment.GetEnvironmentVariable("KUBECONFIG"));
+
+// Load from in-cluster configuration:
+var config = KubernetesClientConfiguration.InClusterConfig()
+
+// Use the config object to create a client.
+var client = new Kubernetes(config);
```
-## Generating code
+### Listing Objects
+```c#
+var namespaces = client.CoreV1.ListNamespace();
+foreach (var ns in namespaces.Items) {
+ Console.WriteLine(ns.Metadata.Name);
+ var list = client.CoreV1.ListNamespacedPod(ns.Metadata.Name);
+ foreach (var item in list.Items)
+ {
+ Console.WriteLine(item.Metadata.Name);
+ }
+}
+```
-```bash
-# Where REPO_DIR points to the root of the csharp repository
-cd ${REPO_DIR}/csharp/src
-${GEN_DIR}/openapi/csharp.sh generated csharp.settings
+### Creating and Deleting Objects
+```c#
+var ns = new V1Namespace
+{
+ Metadata = new V1ObjectMeta
+ {
+ Name = "test"
+ }
+};
+
+var result = client.CoreV1.CreateNamespace(ns);
+Console.WriteLine(result);
+
+var status = client.CoreV1.DeleteNamespace(ns.Metadata.Name, new V1DeleteOptions());
```
-# Usage
+## Examples
+
+There is extensive example code in the [examples directory](https://github.com/kubernetes-client/csharp/tree/master/examples).
-## Running the Examples
+### Running the examples
```bash
git clone git@github.com:kubernetes-client/csharp.git
@@ -46,14 +92,105 @@ cd csharp\examples\simple
dotnet run
```
+## Known issues
+
+While the preferred way of connecting to a remote cluster from local machine is:
+
+```c#
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+var client = new Kubernetes(config);
+```
+
+Not all auth providers are supported at the moment [#91](https://github.com/kubernetes-client/csharp/issues/91#issuecomment-362920478). You can still connect to a cluster by starting the proxy command:
+
+```bash
+$ kubectl proxy
+Starting to serve on 127.0.0.1:8001
+```
+
+and changing config:
+
+```c#
+var config = new KubernetesClientConfiguration { Host = "http://127.0.0.1:8001/" };
+```
+
+Notice that this is a workaround and is not recommended for production use.
+
## Testing
-The project uses [XUnit](https://xunit.github.io) as unit testing framework.
+The project uses [XUnit](https://github.com/xunit/xunit) as unit testing framework.
-To run the tests
+To run the tests:
```bash
cd csharp\tests
dotnet restore
dotnet test
```
+
+# Update the API model
+
+## Prerequisites
+
+You'll need a Linux machine with Docker.
+
+Check out the generator project into some other directory
+(henceforth `$GEN_DIR`).
+
+```bash
+cd $GEN_DIR/..
+git clone https://github.com/kubernetes-client/gen
+```
+
+## Generating new swagger.json
+
+```bash
+# Where REPO_DIR points to the root of the csharp repository
+cd
+${GEN_DIR}/openapi/csharp.sh ${REPO_DIR}/src/KubernetesClient ${REPO_DIR}/csharp.settings
+```
+
+# Version Compatibility
+
+| SDK Version | Kubernetes Version | .NET Targeting |
+|-------------|--------------------|-----------------------------------------------------|
+| 18.0 | 1.34 | net8.0;net9.0;net48*;netstandard2.0* |
+| 17.0 | 1.33 | net8.0;net9.0;net48*;netstandard2.0* |
+| 16.0 | 1.32 | net8.0;net9.0;net48*;netstandard2.0* |
+| 15.0 | 1.31 | net6.0;net8.0;net48*;netstandard2.0* |
+| 14.0 | 1.30 | net6.0;net8.0;net48*;netstandard2.0* |
+| 13.0 | 1.29 | net6.0;net7.0;net8.0;net48*;netstandard2.0* |
+| 12.0 | 1.28 | net6.0;net7.0;net48*;netstandard2.0* |
+| 11.0 | 1.27 | net6.0;net7.0;net48*;netstandard2.0* |
+| 10.0 | 1.26 | net6.0;net7.0;net48*;netstandard2.0* |
+| 9.1 | 1.25 | netstandard2.1;net6.0;net7.0;net48*;netstandard2.0* |
+| 9.0 | 1.25 | netstandard2.1;net5.0;net6.0;net48*;netstandard2.0* |
+| 8.0 | 1.24 | netstandard2.1;net5.0;net6.0;net48*;netstandard2.0* |
+| 7.2 | 1.23 | netstandard2.1;net5.0;net6.0;net48*;netstandard2.0* |
+| 7.0 | 1.23 | netstandard2.1;net5.0;net6.0 |
+| 6.0 | 1.22 | netstandard2.1;net5.0 |
+| 5.0 | 1.21 | netstandard2.1;net5 |
+| 4.0 | 1.20 | netstandard2.0;netstandard2.1 |
+| 3.0 | 1.19 | netstandard2.0;net452 |
+| 2.0 | 1.18 | netstandard2.0;net452 |
+| 1.6 | 1.16 | netstandard1.4;netstandard2.0;net452; |
+| 1.4 | 1.13 | netstandard1.4;net451 |
+| 1.3 | 1.12 | netstandard1.4;net452 |
+
+ * Starting from `2.0`, [dotnet sdk versioning](https://github.com/kubernetes-client/csharp/issues/400) adopted
+ * `Kubernetes Version` here means the version sdk models and apis were generated from
+ * Kubernetes api server guarantees the compatibility with `n-2` (`n-3` after 1.28) version. for example:
+ - 1.19 based sdk should work with 1.21 cluster, but not guaranteed to work with 1.22 cluster.
+
+ and vice versa:
+ - 1.21 based sdk should work with 1.19 cluster, but not guaranteed to work with 1.18 cluster.
+Note: in practice, the sdk might work with much older clusters, at least for the more stable functionality. However, it is not guaranteed past the `n-2` (or `n-3` after 1.28 ) version. See [#1511](https://github.com/kubernetes-client/csharp/issues/1511) for additional details.
+
+ see also
+ * Fixes (including security fixes) are not back-ported automatically to older sdk versions. However, contributions from the community are welcomed 😊; See [Contributing](#contributing) for instructions on how to contribute.
+ * `*` `KubernetesClient.Classic`: netstandard2.0 and net48 are supported with limited features
+
+
+## Contributing
+
+Please see [CONTRIBUTING.md](CONTRIBUTING.md) for instructions on how to contribute.
diff --git a/SECURITY_CONTACTS b/SECURITY_CONTACTS
new file mode 100644
index 000000000..df0df1c5f
--- /dev/null
+++ b/SECURITY_CONTACTS
@@ -0,0 +1,14 @@
+# Defined below are the security contacts for this repo.
+#
+# They are the contact point for the Product Security Team to reach out
+# to for triaging and handling of incoming issues.
+#
+# The below names agree to abide by the
+# [Embargo Policy](https://github.com/kubernetes/sig-release/blob/master/security-release-process-documentation/security-release-process.md#embargo-policy)
+# and will be removed and replaced if they violate that agreement.
+#
+# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
+# INSTRUCTIONS AT https://kubernetes.io/security/
+
+brendandburns
+tg123
diff --git a/ci.sh b/ci.sh
deleted file mode 100755
index f40f2fd65..000000000
--- a/ci.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env bash
-
-# Exit on any error
-set -e
-
-# Ensure no compile errors in all projects
-find . -name *.csproj -exec dotnet build {} \;
-
-# Execute Unit tests
-cd tests
-dotnet restore
-dotnet test
diff --git a/code-of-conduct.md b/code-of-conduct.md
new file mode 100644
index 000000000..0d15c00cf
--- /dev/null
+++ b/code-of-conduct.md
@@ -0,0 +1,3 @@
+# Kubernetes Community Code of Conduct
+
+Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md)
diff --git a/csharp.settings b/csharp.settings
index bd57dc142..0110958c8 100644
--- a/csharp.settings
+++ b/csharp.settings
@@ -1,3 +1,3 @@
-export KUBERNETES_BRANCH=v1.8.4
+export KUBERNETES_BRANCH=v1.34.0
export CLIENT_VERSION=0.0.1
export PACKAGE_NAME=k8s
diff --git a/doc/.gitignore b/doc/.gitignore
new file mode 100644
index 000000000..2f16432e9
--- /dev/null
+++ b/doc/.gitignore
@@ -0,0 +1,11 @@
+###############
+# folder #
+###############
+/**/DROP/
+/**/TEMP/
+/**/packages/
+/**/bin/
+/**/obj/
+_site
+
+api
\ No newline at end of file
diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md
new file mode 120000
index 000000000..44fcc6343
--- /dev/null
+++ b/doc/CONTRIBUTING.md
@@ -0,0 +1 @@
+../CONTRIBUTING.md
\ No newline at end of file
diff --git a/doc/docfx.json b/doc/docfx.json
new file mode 100644
index 000000000..2917802e6
--- /dev/null
+++ b/doc/docfx.json
@@ -0,0 +1,41 @@
+{
+ "metadata": [
+ {
+ "src": [
+ {
+ "files": [
+ "KubernetesClient/bin/Release/net8.0/KubernetesClient.dll"
+ ],
+ "src": "../src"
+ }
+ ],
+ "dest": "api",
+ "disableGitFeatures": false,
+ "disableDefaultFilter": false
+ }
+ ],
+ "build": {
+ "content": [
+ {
+ "files": [
+ "api/**.yml",
+ "index.md",
+ "CONTRIBUTING.md",
+ "toc.yml"
+ ]
+ }
+ ],
+ "dest": "_site",
+ "globalMetadataFiles": [],
+ "fileMetadataFiles": [],
+ "template": [
+ "default"
+ ],
+ "postProcessors": [],
+ "markdownEngineName": "markdig",
+ "noLangKeyword": false,
+ "keepFileLink": false,
+ "cleanupCacheHistory": false,
+ "disableGitFeatures": false
+ }
+}
\ No newline at end of file
diff --git a/doc/index.md b/doc/index.md
new file mode 120000
index 000000000..32d46ee88
--- /dev/null
+++ b/doc/index.md
@@ -0,0 +1 @@
+../README.md
\ No newline at end of file
diff --git a/doc/toc.yml b/doc/toc.yml
new file mode 100644
index 000000000..8bf2c8ed1
--- /dev/null
+++ b/doc/toc.yml
@@ -0,0 +1,2 @@
+- name: API Documentation
+ href: api/k8s.yml
diff --git a/examples/Directory.Build.props b/examples/Directory.Build.props
new file mode 100644
index 000000000..b87fe6aaa
--- /dev/null
+++ b/examples/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+
+
+ net9.0
+
+
diff --git a/examples/Directory.Build.targets b/examples/Directory.Build.targets
new file mode 100644
index 000000000..bf5f5ee49
--- /dev/null
+++ b/examples/Directory.Build.targets
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/examples/aks-kubelogin/Program.cs b/examples/aks-kubelogin/Program.cs
new file mode 100644
index 000000000..cdee0cf10
--- /dev/null
+++ b/examples/aks-kubelogin/Program.cs
@@ -0,0 +1,49 @@
+using k8s;
+using System;
+using System.IO;
+using System.Text;
+
+var server = "https://example.hcp.eastus.azmk8s.io/"; // the server url of your aks
+var clientid = "00000000-0000-0000-0000-000000000000"; // the client id of the your msi
+var kubelogin = @"C:\bin\kubelogin.exe"; // the path to the kubelogin.exe
+
+using var configstream = new MemoryStream(Encoding.ASCII.GetBytes($"""
+apiVersion: v1
+clusters:
+- cluster:
+ insecure-skip-tls-verify: true
+ server: {server}
+ name: aks
+contexts:
+- context:
+ cluster: aks
+ user: msi
+ name: aks
+current-context: aks
+kind: Config
+users:
+- name: msi
+ user:
+ exec:
+ apiVersion: client.authentication.k8s.io/v1beta1
+ args:
+ - get-token
+ - --login
+ - msi
+ - --server-id
+ - 6dae42f8-4368-4678-94ff-3960e28e3630
+ - --client-id
+ - {clientid}
+ command: {kubelogin}
+ env: null
+"""));
+
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile(configstream);
+IKubernetes client = new Kubernetes(config);
+Console.WriteLine("Starting Request!");
+
+var list = client.CoreV1.ListNamespacedPod("default");
+foreach (var item in list.Items)
+{
+ Console.WriteLine(item.Metadata.Name);
+}
diff --git a/examples/aks-kubelogin/README.md b/examples/aks-kubelogin/README.md
new file mode 100644
index 000000000..ab71071b0
--- /dev/null
+++ b/examples/aks-kubelogin/README.md
@@ -0,0 +1,24 @@
+# AKS C# example using kubelogin + MSI
+
+This example shows how to use the [kubelogin](https://github.com/Azure/kubelogin) to authenticate using [managed identities](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) with Azure Kubernetes Service (AKS) using the C# SDK.
+
+
+## Prerequisites
+
+ - turn on AAD support for AKS, see [here](https://docs.microsoft.com/en-us/azure/aks/managed-aad)
+ - create a managed identity for the AKS cluster
+ - assign the managed identity the `Azure Kubernetes Service RBAC Cluster Admin` (or other RBAC permission) on the AKS cluster
+ - assign the managed identity to the VM, see [here](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm)
+ - install the [kubelogin](https://github.com/Azure/kubelogin) to your machine
+
+## Running the code
+
+ *You must the the code on VM with MSI*
+
+ - Replace `server` with the address of your AKS cluster
+ - Replace `clientid` with the client id of the managed identity
+ - Replace `kubelogin` with the path to the kubelogin executable
+
+```
+dotnet run
+```
\ No newline at end of file
diff --git a/examples/aks-kubelogin/aks-kubelogin.csproj b/examples/aks-kubelogin/aks-kubelogin.csproj
new file mode 100644
index 000000000..11afe8d56
--- /dev/null
+++ b/examples/aks-kubelogin/aks-kubelogin.csproj
@@ -0,0 +1,5 @@
+
+
+ Exe
+
+
\ No newline at end of file
diff --git a/examples/aot/Program.cs b/examples/aot/Program.cs
new file mode 100644
index 000000000..d5125c0ff
--- /dev/null
+++ b/examples/aot/Program.cs
@@ -0,0 +1,16 @@
+using k8s;
+
+var config = KubernetesClientConfiguration.BuildDefaultConfig();
+IKubernetes client = new Kubernetes(config);
+Console.WriteLine("Starting Request!");
+
+var list = client.CoreV1.ListNamespacedPod("default");
+foreach (var item in list.Items)
+{
+ Console.WriteLine(item.Metadata.Name);
+}
+
+if (list.Items.Count == 0)
+{
+ Console.WriteLine("Empty!");
+}
\ No newline at end of file
diff --git a/examples/aot/aot.csproj b/examples/aot/aot.csproj
new file mode 100644
index 000000000..28741906d
--- /dev/null
+++ b/examples/aot/aot.csproj
@@ -0,0 +1,11 @@
+
+
+ Exe
+ enable
+ enable
+ true
+
+
+
+
+
diff --git a/examples/attach/Attach.cs b/examples/attach/Attach.cs
new file mode 100755
index 000000000..cfdce7d8e
--- /dev/null
+++ b/examples/attach/Attach.cs
@@ -0,0 +1,31 @@
+using k8s;
+using k8s.Models;
+using System;
+using System.Threading.Tasks;
+
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+IKubernetes client = new Kubernetes(config);
+Console.WriteLine("Starting Request!");
+
+var list = client.CoreV1.ListNamespacedPod("default");
+var pod = list.Items[0];
+await AttachToPod(client, pod).ConfigureAwait(false);
+
+async Task AttachToPod(IKubernetes client, V1Pod pod)
+{
+ var webSocket =
+ await client.WebSocketNamespacedPodAttachAsync(pod.Metadata.Name, "default",
+ pod.Spec.Containers[0].Name).ConfigureAwait(false);
+
+ var demux = new StreamDemuxer(webSocket);
+ demux.Start();
+
+ var buff = new byte[4096];
+ var stream = demux.GetStream(1, 1);
+ while (true)
+ {
+ var read = stream.Read(buff, 0, 4096);
+ var str = System.Text.Encoding.Default.GetString(buff);
+ Console.WriteLine(str);
+ }
+}
diff --git a/examples/attach/attach.csproj b/examples/attach/attach.csproj
new file mode 100755
index 000000000..5644cab8a
--- /dev/null
+++ b/examples/attach/attach.csproj
@@ -0,0 +1,7 @@
+
+
+
+ Exe
+
+
+
diff --git a/examples/clientset/Program.cs b/examples/clientset/Program.cs
new file mode 100644
index 000000000..a1b74e0f8
--- /dev/null
+++ b/examples/clientset/Program.cs
@@ -0,0 +1,32 @@
+using k8s;
+using k8s.Models;
+using k8s.ClientSets;
+using System.Threading.Tasks;
+
+namespace clientset
+{
+ internal class Program
+ {
+ private static async Task Main(string[] args)
+ {
+ var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+ var client = new Kubernetes(config);
+
+ var clientSet = new ClientSet(client);
+ var list = await clientSet.CoreV1.Pod.ListAsync("default").ConfigureAwait(false);
+ foreach (var item in list)
+ {
+ System.Console.WriteLine(item.Metadata.Name);
+ }
+
+ var pod = await clientSet.CoreV1.Pod.GetAsync("test", "default").ConfigureAwait(false);
+ System.Console.WriteLine(pod?.Metadata?.Name);
+
+ var watch = clientSet.CoreV1.Pod.WatchListAsync("default");
+ await foreach (var (_, item) in watch.ConfigureAwait(false))
+ {
+ System.Console.WriteLine(item.Metadata.Name);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/clientset/clientset.csproj b/examples/clientset/clientset.csproj
new file mode 100644
index 000000000..4274ceb02
--- /dev/null
+++ b/examples/clientset/clientset.csproj
@@ -0,0 +1,6 @@
+
+
+ Exe
+
+
+
diff --git a/examples/cp/Cp.cs b/examples/cp/Cp.cs
new file mode 100644
index 000000000..43e769490
--- /dev/null
+++ b/examples/cp/Cp.cs
@@ -0,0 +1,110 @@
+using ICSharpCode.SharpZipLib.Tar;
+using k8s;
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace cp;
+
+internal class Cp
+{
+ private static IKubernetes client;
+
+ private static async Task Main(string[] args)
+ {
+ var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+ client = new Kubernetes(config);
+
+
+ var pods = client.CoreV1.ListNamespacedPod("default", null, null, null, $"job-name=upload-demo");
+ var pod = pods.Items.First();
+
+ await CopyFileToPodAsync(pod.Metadata.Name, "default", "upload-demo", args[0], $"home/{args[1]}").ConfigureAwait(false);
+ }
+
+
+
+
+ private static void ValidatePathParameters(string sourcePath, string destinationPath)
+ {
+ if (string.IsNullOrWhiteSpace(sourcePath))
+ {
+ throw new ArgumentException($"{nameof(sourcePath)} cannot be null or whitespace");
+ }
+
+ if (string.IsNullOrWhiteSpace(destinationPath))
+ {
+ throw new ArgumentException($"{nameof(destinationPath)} cannot be null or whitespace");
+ }
+ }
+
+ public static async Task CopyFileToPodAsync(string name, string @namespace, string container, string sourceFilePath, string destinationFilePath, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ // All other parameters are being validated by MuxedStreamNamespacedPodExecAsync called by NamespacedPodExecAsync
+ ValidatePathParameters(sourceFilePath, destinationFilePath);
+
+ // The callback which processes the standard input, standard output and standard error of exec method
+ var handler = new ExecAsyncCallback(async (stdIn, stdOut, stdError) =>
+ {
+ var fileInfo = new FileInfo(destinationFilePath);
+ try
+ {
+ using (var memoryStream = new MemoryStream())
+ {
+ using (var inputFileStream = File.OpenRead(sourceFilePath))
+ using (var tarOutputStream = new TarOutputStream(memoryStream, Encoding.Default))
+ {
+ tarOutputStream.IsStreamOwner = false;
+
+ var fileSize = inputFileStream.Length;
+ var entry = TarEntry.CreateTarEntry(fileInfo.Name);
+
+ entry.Size = fileSize;
+
+ tarOutputStream.PutNextEntry(entry);
+ await inputFileStream.CopyToAsync(tarOutputStream).ConfigureAwait(false);
+ tarOutputStream.CloseEntry();
+ }
+
+ memoryStream.Position = 0;
+
+ await memoryStream.CopyToAsync(stdIn).ConfigureAwait(false);
+ await stdIn.FlushAsync().ConfigureAwait(false);
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new IOException($"Copy command failed: {ex.Message}");
+ }
+
+ using StreamReader streamReader = new StreamReader(stdError);
+ while (streamReader.EndOfStream == false)
+ {
+ string error = await streamReader.ReadToEndAsync().ConfigureAwait(false);
+ throw new IOException($"Copy command failed: {error}");
+ }
+ });
+
+ string destinationFolder = GetFolderName(destinationFilePath);
+
+ return await client.NamespacedPodExecAsync(
+ name,
+ @namespace,
+ container,
+ new string[] { "sh", "-c", $"tar xmf - -C {destinationFolder}" },
+ false,
+ handler,
+ cancellationToken).ConfigureAwait(false);
+ }
+
+
+ private static string GetFolderName(string filePath)
+ {
+ var folderName = Path.GetDirectoryName(filePath);
+
+ return string.IsNullOrEmpty(folderName) ? "." : folderName;
+ }
+}
diff --git a/examples/cp/cp.csproj b/examples/cp/cp.csproj
new file mode 100644
index 000000000..cde0f0fca
--- /dev/null
+++ b/examples/cp/cp.csproj
@@ -0,0 +1,11 @@
+
+
+
+ Exe
+
+
+
+
+
+
+
diff --git a/examples/csrApproval/Program.cs b/examples/csrApproval/Program.cs
new file mode 100644
index 000000000..6c374105b
--- /dev/null
+++ b/examples/csrApproval/Program.cs
@@ -0,0 +1,85 @@
+using Json.Patch;
+using k8s;
+using k8s.Models;
+using System.Net;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Text.Json;
+
+string GenerateCertificate(string name)
+{
+ var sanBuilder = new SubjectAlternativeNameBuilder();
+ sanBuilder.AddIpAddress(IPAddress.Loopback);
+ sanBuilder.AddIpAddress(IPAddress.IPv6Loopback);
+ sanBuilder.AddDnsName("localhost");
+ sanBuilder.AddDnsName(Environment.MachineName);
+
+ var distinguishedName = new X500DistinguishedName(name);
+
+ using var rsa = RSA.Create(4096);
+ var request = new CertificateRequest(distinguishedName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+
+ request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));
+ request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension([new ("1.3.6.1.5.5.7.3.1")], false));
+ request.CertificateExtensions.Add(sanBuilder.Build());
+ var csr = request.CreateSigningRequest();
+ var pemKey = "-----BEGIN CERTIFICATE REQUEST-----\r\n" +
+ Convert.ToBase64String(csr) +
+ "\r\n-----END CERTIFICATE REQUEST-----";
+
+ return pemKey;
+}
+
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+IKubernetes client = new Kubernetes(config);
+Console.WriteLine("Starting Request!");
+var name = "demo";
+var x509 = GenerateCertificate(name);
+var encodedCsr = Encoding.UTF8.GetBytes(x509);
+
+var request = new V1CertificateSigningRequest
+{
+ ApiVersion = "certificates.k8s.io/v1",
+ Kind = "CertificateSigningRequest",
+ Metadata = new V1ObjectMeta
+ {
+ Name = name,
+ },
+ Spec = new V1CertificateSigningRequestSpec
+ {
+ Request = encodedCsr,
+ SignerName = "kubernetes.io/kube-apiserver-client",
+ Usages = new List { "client auth" },
+ ExpirationSeconds = 600, // minimum should be 10 minutes
+ },
+};
+
+await client.CertificatesV1.CreateCertificateSigningRequestAsync(request).ConfigureAwait(false);
+
+var serializeOptions = new JsonSerializerOptions
+{
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = true,
+};
+var readCert = await client.CertificatesV1.ReadCertificateSigningRequestAsync(name).ConfigureAwait(false);
+var old = JsonSerializer.SerializeToDocument(readCert, serializeOptions);
+
+var replace = new List
+{
+ new V1CertificateSigningRequestCondition
+ {
+ Type = "Approved",
+ Status = "True",
+ Reason = "Approve",
+ Message = "This certificate was approved by k8s client",
+ LastUpdateTime = DateTime.UtcNow,
+ LastTransitionTime = DateTime.UtcNow,
+ },
+};
+readCert.Status.Conditions = replace;
+
+var expected = JsonSerializer.SerializeToDocument(readCert, serializeOptions);
+
+var patch = old.CreatePatch(expected);
+await client.CertificatesV1.PatchCertificateSigningRequestApprovalAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name).ConfigureAwait(false);
diff --git a/examples/csrApproval/csrApproval.csproj b/examples/csrApproval/csrApproval.csproj
new file mode 100644
index 000000000..f67de4fbc
--- /dev/null
+++ b/examples/csrApproval/csrApproval.csproj
@@ -0,0 +1,12 @@
+
+
+
+ Exe
+ enable
+ enable
+
+
+
+
+
+
diff --git a/examples/customResource/CustomResourceDefinition.cs b/examples/customResource/CustomResourceDefinition.cs
new file mode 100644
index 000000000..ad1b7f9c4
--- /dev/null
+++ b/examples/customResource/CustomResourceDefinition.cs
@@ -0,0 +1,45 @@
+using k8s;
+using k8s.Models;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "CA1724:TypeNamesShouldNotMatchNamespaces", Justification = "This is just an example.")]
+[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleClass", Justification = "This is just an example.")]
+
+namespace customResource
+{
+ public class CustomResourceDefinition
+ {
+ public string Version { get; set; }
+
+ public string Group { get; set; }
+
+ public string PluralName { get; set; }
+
+ public string Kind { get; set; }
+
+ public string Namespace { get; set; }
+ }
+
+ public abstract class CustomResource : KubernetesObject, IMetadata
+ {
+ [JsonPropertyName("metadata")]
+ public V1ObjectMeta Metadata { get; set; }
+ }
+
+ public abstract class CustomResource : CustomResource
+ {
+ [JsonPropertyName("spec")]
+ public TSpec Spec { get; set; }
+
+ [JsonPropertyName("status")]
+ public TStatus Status { get; set; }
+ }
+
+ public class CustomResourceList : KubernetesObject
+ where T : CustomResource
+ {
+ public V1ListMeta Metadata { get; set; }
+ public List Items { get; set; }
+ }
+}
diff --git a/examples/customResource/Program.cs b/examples/customResource/Program.cs
new file mode 100644
index 000000000..726852a7f
--- /dev/null
+++ b/examples/customResource/Program.cs
@@ -0,0 +1,107 @@
+using Json.Patch;
+using k8s;
+using k8s.Autorest;
+using k8s.Models;
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+
+namespace customResource
+{
+ public class Program
+ {
+ private static async Task Main(string[] args)
+ {
+ Console.WriteLine("starting main()...");
+
+ // creating the k8s client
+ var k8SClientConfig = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+ IKubernetes client = new Kubernetes(k8SClientConfig);
+
+ // creating a K8s client for the CRD
+ var myCRD = Utils.MakeCRD();
+ Console.WriteLine("working with CRD: {0}.{1}", myCRD.PluralName, myCRD.Group);
+ var generic = new GenericClient(client, myCRD.Group, myCRD.Version, myCRD.PluralName);
+
+ // creating a sample custom resource content
+ var myCr = Utils.MakeCResource();
+
+ try
+ {
+ Console.WriteLine("creating CR {0}", myCr.Metadata.Name);
+ var response = await client.CustomObjects.CreateNamespacedCustomObjectWithHttpMessagesAsync(
+ myCr,
+ myCRD.Group, myCRD.Version,
+ myCr.Metadata.NamespaceProperty ?? "default",
+ myCRD.PluralName).ConfigureAwait(false);
+ }
+ catch (HttpOperationException httpOperationException) when (httpOperationException.Message.Contains("422"))
+ {
+ var phase = httpOperationException.Response.ReasonPhrase;
+ var content = httpOperationException.Response.Content;
+ Console.WriteLine("response content: {0}", content);
+ Console.WriteLine("response phase: {0}", phase);
+ }
+ catch (HttpOperationException)
+ {
+ }
+
+ // listing the cr instances
+ Console.WriteLine("CR list:");
+ var crs = await generic.ListNamespacedAsync>(myCr.Metadata.NamespaceProperty ?? "default").ConfigureAwait(false);
+ foreach (var cr in crs.Items)
+ {
+ Console.WriteLine("- CR Item {0} = {1}", crs.Items.IndexOf(cr), cr.Metadata.Name);
+ }
+
+ var old = JsonSerializer.SerializeToDocument(myCr);
+ myCr.Metadata.Labels.TryAdd("newKey", "newValue");
+
+ var expected = JsonSerializer.SerializeToDocument(myCr);
+ var patch = old.CreatePatch(expected);
+
+ // updating the custom resource
+ var crPatch = new V1Patch(patch, V1Patch.PatchType.JsonPatch);
+ try
+ {
+ var patchResponse = await client.CustomObjects.PatchNamespacedCustomObjectAsync(
+ crPatch,
+ myCRD.Group,
+ myCRD.Version,
+ myCr.Metadata.NamespaceProperty ?? "default",
+ myCRD.PluralName,
+ myCr.Metadata.Name).ConfigureAwait(false);
+ }
+ catch (HttpOperationException httpOperationException)
+ {
+ var phase = httpOperationException.Response.ReasonPhrase;
+ var content = httpOperationException.Response.Content;
+ Console.WriteLine("response content: {0}", content);
+ Console.WriteLine("response phase: {0}", phase);
+ }
+
+ // getting the updated custom resource
+ var fetchedCR = await generic.ReadNamespacedAsync(
+ myCr.Metadata.NamespaceProperty ?? "default",
+ myCr.Metadata.Name).ConfigureAwait(false);
+
+ Console.WriteLine("fetchedCR = {0}", fetchedCR.ToString());
+
+ // deleting the custom resource
+ try
+ {
+ var status = await generic.DeleteNamespacedAsync(
+ myCr.Metadata.NamespaceProperty ?? "default",
+ myCr.Metadata.Name).ConfigureAwait(false);
+
+ Console.WriteLine($"Deleted the CR status: {status}");
+ }
+ catch (Exception exception)
+ {
+ Console.WriteLine("Exception type {0}", exception);
+ }
+ }
+ }
+}
diff --git a/examples/customResource/README.md b/examples/customResource/README.md
new file mode 100644
index 000000000..2fdb11703
--- /dev/null
+++ b/examples/customResource/README.md
@@ -0,0 +1,53 @@
+# Custom Resource Client Example
+
+This example demonstrates how to use the C# Kubernetes Client library to create, get and list custom resources.
+
+## Pre-requisits
+
+Make sure your have added the library package
+
+```shell
+dotnet add package KubernetesClient
+```
+
+## Create Custom Resource Definition (CRD)
+
+Make sure the [CRD](./config/crd.yaml) is created, in order to create an instance of it after.
+
+```shell
+kubectl create -f ./config/crd.yaml
+```
+
+You can test that the CRD is successfully added, by creating an [instance](./config/yaml-cr-instance.yaml) of it using kubectl:
+
+```shell
+kubectl create -f ./config/yaml-cr-instance.yaml
+```
+
+```shell
+kubectl get customresources.csharp.com
+```
+
+## Execute the code
+
+The client uses the `BuildConfigFromConfigFile()` function. If the KUBECONFIG environment variable is set, then that path to the k8s config file will be used.
+
+`dotnet run`
+
+Expected output:
+
+```
+strating main()...
+working with CRD: customresources.csharp.com
+creating CR cr-instance-london
+CR list:
+- CR Item 0 = cr-instance-london
+- CR Item 1 = cr-instance-paris
+fetchedCR = cr-instance-london (Labels: {identifier : city, newKey : newValue}), Spec: London
+Deleted the CR
+```
+
+## Under the hood
+
+For more details, you can look at the Generic client [implementation](https://github.com/kubernetes-client/csharp/blob/master/src/KubernetesClient/GenericClient.cs)
+
diff --git a/examples/customResource/Utils.cs b/examples/customResource/Utils.cs
new file mode 100644
index 000000000..8101c1d38
--- /dev/null
+++ b/examples/customResource/Utils.cs
@@ -0,0 +1,48 @@
+using k8s.Models;
+using System.Collections.Generic;
+namespace customResource
+{
+ public class Utils
+ {
+ // creats a CRD definition
+ public static CustomResourceDefinition MakeCRD()
+ {
+ var myCRD = new CustomResourceDefinition()
+ {
+ Kind = "CResource",
+ Group = "csharp.com",
+ Version = "v1alpha1",
+ PluralName = "customresources",
+ };
+
+ return myCRD;
+ }
+
+ // creats a CR instance
+ public static CResource MakeCResource()
+ {
+ var myCResource = new CResource()
+ {
+ Kind = "CResource",
+ ApiVersion = "csharp.com/v1alpha1",
+ Metadata = new V1ObjectMeta
+ {
+ Name = "cr-instance-london",
+ NamespaceProperty = "default",
+ Labels = new Dictionary
+ {
+ {
+ "identifier", "city"
+ },
+ },
+ },
+ // spec
+ Spec = new CResourceSpec
+ {
+ CityName = "London",
+ },
+ };
+ return myCResource;
+ }
+ }
+}
diff --git a/examples/customResource/cResource.cs b/examples/customResource/cResource.cs
new file mode 100644
index 000000000..67440aee9
--- /dev/null
+++ b/examples/customResource/cResource.cs
@@ -0,0 +1,33 @@
+using k8s.Models;
+using System.Text.Json.Serialization;
+
+namespace customResource
+{
+ public class CResource : CustomResource
+ {
+ public override string ToString()
+ {
+ var labels = "{";
+ foreach (var kvp in Metadata.Labels)
+ {
+ labels += kvp.Key + " : " + kvp.Value + ", ";
+ }
+
+ labels = labels.TrimEnd(',', ' ') + "}";
+
+ return $"{Metadata.Name} (Labels: {labels}), Spec: {Spec.CityName}";
+ }
+ }
+
+ public record CResourceSpec
+ {
+ [JsonPropertyName("cityName")]
+ public string CityName { get; set; }
+ }
+
+ public record CResourceStatus : V1Status
+ {
+ [JsonPropertyName("temperature")]
+ public string Temperature { get; set; }
+ }
+}
diff --git a/examples/customResource/config/crd.yaml b/examples/customResource/config/crd.yaml
new file mode 100644
index 000000000..424546f14
--- /dev/null
+++ b/examples/customResource/config/crd.yaml
@@ -0,0 +1,23 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: customresources.csharp.com
+spec:
+ group: csharp.com
+ versions:
+ - name: v1alpha1
+ storage: true
+ served: true
+ schema:
+ openAPIV3Schema:
+ type: object
+ properties:
+ spec:
+ type: object
+ properties:
+ cityName:
+ type: string
+ names:
+ kind: CResource
+ plural: customresources
+ scope: Namespaced
diff --git a/examples/customResource/config/yaml-cr-instance.yaml b/examples/customResource/config/yaml-cr-instance.yaml
new file mode 100644
index 000000000..482dd48ff
--- /dev/null
+++ b/examples/customResource/config/yaml-cr-instance.yaml
@@ -0,0 +1,8 @@
+apiVersion: csharp.com/v1alpha1
+kind: CResource
+metadata:
+ name: cr-instance-paris
+ namespace: default
+spec:
+ cityName: Paris
+
\ No newline at end of file
diff --git a/examples/customResource/customResource.csproj b/examples/customResource/customResource.csproj
new file mode 100644
index 000000000..ad2bdd739
--- /dev/null
+++ b/examples/customResource/customResource.csproj
@@ -0,0 +1,11 @@
+
+
+
+ Exe
+
+
+
+
+
+
+
diff --git a/examples/exec/Exec.cs b/examples/exec/Exec.cs
new file mode 100755
index 000000000..20bbd2125
--- /dev/null
+++ b/examples/exec/Exec.cs
@@ -0,0 +1,28 @@
+using k8s;
+using k8s.Models;
+using System;
+using System.Threading.Tasks;
+
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+IKubernetes client = new Kubernetes(config);
+Console.WriteLine("Starting Request!");
+
+var list = client.CoreV1.ListNamespacedPod("default");
+var pod = list.Items[0];
+await ExecInPod(client, pod).ConfigureAwait(false);
+
+async Task ExecInPod(IKubernetes client, V1Pod pod)
+{
+ var webSocket =
+ await client.WebSocketNamespacedPodExecAsync(pod.Metadata.Name, "default", "ls",
+ pod.Spec.Containers[0].Name).ConfigureAwait(false);
+
+ var demux = new StreamDemuxer(webSocket);
+ demux.Start();
+
+ var buff = new byte[4096];
+ var stream = demux.GetStream(1, 1);
+ var read = stream.Read(buff, 0, 4096);
+ var str = System.Text.Encoding.Default.GetString(buff);
+ Console.WriteLine(str);
+}
diff --git a/examples/exec/exec.csproj b/examples/exec/exec.csproj
new file mode 100755
index 000000000..52e6553de
--- /dev/null
+++ b/examples/exec/exec.csproj
@@ -0,0 +1,7 @@
+
+
+
+ Exe
+
+
+
diff --git a/examples/generic/Generic.cs b/examples/generic/Generic.cs
new file mode 100644
index 000000000..f65fb944d
--- /dev/null
+++ b/examples/generic/Generic.cs
@@ -0,0 +1,16 @@
+using k8s;
+using k8s.Models;
+using System;
+
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+IKubernetes client = new Kubernetes(config);
+var generic = new GenericClient(client, "", "v1", "nodes");
+var node = await generic.ReadAsync("kube0").ConfigureAwait(false);
+Console.WriteLine(node.Metadata.Name);
+
+var genericPods = new GenericClient(client, "", "v1", "pods");
+var pods = await genericPods.ListNamespacedAsync("default").ConfigureAwait(false);
+foreach (var pod in pods.Items)
+{
+ Console.WriteLine(pod.Metadata.Name);
+}
diff --git a/examples/generic/generic.csproj b/examples/generic/generic.csproj
new file mode 100644
index 000000000..52e6553de
--- /dev/null
+++ b/examples/generic/generic.csproj
@@ -0,0 +1,7 @@
+
+
+
+ Exe
+
+
+
diff --git a/examples/labels/PodList.cs b/examples/labels/PodList.cs
new file mode 100755
index 000000000..0c5df001d
--- /dev/null
+++ b/examples/labels/PodList.cs
@@ -0,0 +1,39 @@
+using k8s;
+using System;
+using System.Collections.Generic;
+
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+IKubernetes client = new Kubernetes(config);
+Console.WriteLine("Starting Request!");
+
+var list = client.CoreV1.ListNamespacedService("default");
+foreach (var item in list.Items)
+{
+ Console.WriteLine("Pods for service: " + item.Metadata.Name);
+ Console.WriteLine("=-=-=-=-=-=-=-=-=-=-=");
+ if (item.Spec == null || item.Spec.Selector == null)
+ {
+ continue;
+ }
+
+ var labels = new List();
+ foreach (var key in item.Spec.Selector)
+ {
+ labels.Add(key.Key + "=" + key.Value);
+ }
+
+ var labelStr = string.Join(",", labels.ToArray());
+ Console.WriteLine(labelStr);
+ var podList = client.CoreV1.ListNamespacedPod("default", labelSelector: labelStr);
+ foreach (var pod in podList.Items)
+ {
+ Console.WriteLine(pod.Metadata.Name);
+ }
+
+ if (podList.Items.Count == 0)
+ {
+ Console.WriteLine("Empty!");
+ }
+
+ Console.WriteLine();
+}
diff --git a/examples/labels/labels.csproj b/examples/labels/labels.csproj
new file mode 100755
index 000000000..52e6553de
--- /dev/null
+++ b/examples/labels/labels.csproj
@@ -0,0 +1,7 @@
+
+
+
+ Exe
+
+
+
diff --git a/examples/logs/Logs.cs b/examples/logs/Logs.cs
new file mode 100755
index 000000000..5293de579
--- /dev/null
+++ b/examples/logs/Logs.cs
@@ -0,0 +1,21 @@
+using k8s;
+using System;
+
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+IKubernetes client = new Kubernetes(config);
+Console.WriteLine("Starting Request!");
+
+var list = client.CoreV1.ListNamespacedPod("default");
+if (list.Items.Count == 0)
+{
+ Console.WriteLine("No pods!");
+ return;
+}
+
+var pod = list.Items[0];
+
+var response = await client.CoreV1.ReadNamespacedPodLogWithHttpMessagesAsync(
+ pod.Metadata.Name,
+ pod.Metadata.NamespaceProperty, container: pod.Spec.Containers[0].Name, follow: true).ConfigureAwait(false);
+var stream = response.Body;
+stream.CopyTo(Console.OpenStandardOutput());
diff --git a/examples/logs/logs.csproj b/examples/logs/logs.csproj
new file mode 100755
index 000000000..52e6553de
--- /dev/null
+++ b/examples/logs/logs.csproj
@@ -0,0 +1,7 @@
+
+
+
+ Exe
+
+
+
diff --git a/examples/metrics/Program.cs b/examples/metrics/Program.cs
new file mode 100644
index 000000000..f823bf54d
--- /dev/null
+++ b/examples/metrics/Program.cs
@@ -0,0 +1,51 @@
+using k8s;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+async Task NodesMetrics(IKubernetes client)
+{
+ var nodesMetrics = await client.GetKubernetesNodesMetricsAsync().ConfigureAwait(false);
+
+ foreach (var item in nodesMetrics.Items)
+ {
+ Console.WriteLine(item.Metadata.Name);
+
+ foreach (var metric in item.Usage)
+ {
+ Console.WriteLine($"{metric.Key}: {metric.Value}");
+ }
+ }
+}
+
+async Task PodsMetrics(IKubernetes client)
+{
+ var podsMetrics = await client.GetKubernetesPodsMetricsAsync().ConfigureAwait(false);
+
+ if (!podsMetrics.Items.Any())
+ {
+ Console.WriteLine("Empty");
+ }
+
+ foreach (var item in podsMetrics.Items)
+ {
+ foreach (var container in item.Containers)
+ {
+ Console.WriteLine(container.Name);
+
+ foreach (var metric in container.Usage)
+ {
+ Console.WriteLine($"{metric.Key}: {metric.Value}");
+ }
+ }
+
+ Console.Write(Environment.NewLine);
+ }
+}
+
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+var client = new Kubernetes(config);
+
+await NodesMetrics(client).ConfigureAwait(false);
+Console.WriteLine(Environment.NewLine);
+await PodsMetrics(client).ConfigureAwait(false);
diff --git a/examples/metrics/metrics.csproj b/examples/metrics/metrics.csproj
new file mode 100644
index 000000000..52e6553de
--- /dev/null
+++ b/examples/metrics/metrics.csproj
@@ -0,0 +1,7 @@
+
+
+
+ Exe
+
+
+
diff --git a/examples/namespace/Namespace.cs b/examples/namespace/Namespace.cs
deleted file mode 100644
index 92b85c742..000000000
--- a/examples/namespace/Namespace.cs
+++ /dev/null
@@ -1,87 +0,0 @@
-using System;
-using System.Net;
-using System.Threading.Tasks;
-using k8s;
-using k8s.Models;
-
-namespace @namespace
-{
- class NamespaceExample
- {
- static void ListNamespaces(IKubernetes client) {
- var list = client.ListNamespace();
- foreach (var item in list.Items) {
- Console.WriteLine(item.Metadata.Name);
- }
- if (list.Items.Count == 0) {
- Console.WriteLine("Empty!");
- }
- }
-
- static async Task DeleteAsync(IKubernetes client, string name, int delayMillis) {
- while (true) {
- await Task.Delay(delayMillis);
- try
- {
- await client.ReadNamespaceAsync(name);
- } catch (AggregateException ex) {
- foreach (var innerEx in ex.InnerExceptions) {
- if (innerEx is Microsoft.Rest.HttpOperationException) {
- var code = ((Microsoft.Rest.HttpOperationException)innerEx).Response.StatusCode;
- if (code == HttpStatusCode.NotFound) {
- return;
- }
- throw ex;
- }
- }
- } catch (Microsoft.Rest.HttpOperationException ex) {
- if (ex.Response.StatusCode == HttpStatusCode.NotFound) {
- return;
- }
- throw ex;
- }
- }
- }
-
- static void Delete(IKubernetes client, string name, int delayMillis) {
- DeleteAsync(client, name, delayMillis).Wait();
- }
-
- private static void Main(string[] args)
- {
- var k8SClientConfig = KubernetesClientConfiguration.BuildConfigFromConfigFile();
- IKubernetes client = new Kubernetes(k8SClientConfig);
-
- ListNamespaces(client);
-
- var ns = new V1Namespace
- {
- Metadata = new V1ObjectMeta
- {
- Name = "test"
- }
- };
-
- var result = client.CreateNamespace(ns);
- Console.WriteLine(result);
-
- ListNamespaces(client);
-
- var status = client.DeleteNamespace(new V1DeleteOptions(), ns.Metadata.Name);
-
- if (status.HasObject)
- {
- var obj = status.ObjectView();
- Console.WriteLine(obj.Status.Phase);
-
- Delete(client, ns.Metadata.Name, 3 * 1000);
- }
- else
- {
- Console.WriteLine(status.Message);
- }
-
- ListNamespaces(client);
- }
- }
-}
diff --git a/examples/namespace/NamespaceExample.cs b/examples/namespace/NamespaceExample.cs
new file mode 100644
index 000000000..06e8757a4
--- /dev/null
+++ b/examples/namespace/NamespaceExample.cs
@@ -0,0 +1,89 @@
+using k8s;
+using k8s.Models;
+using System;
+using System.Net;
+using System.Threading.Tasks;
+
+void ListNamespaces(IKubernetes client)
+{
+ var list = client.CoreV1.ListNamespace();
+ foreach (var item in list.Items)
+ {
+ Console.WriteLine(item.Metadata.Name);
+ }
+
+ if (list.Items.Count == 0)
+ {
+ Console.WriteLine("Empty!");
+ }
+}
+
+async Task DeleteAsync(IKubernetes client, string name, int delayMillis)
+{
+ while (true)
+ {
+ await Task.Delay(delayMillis).ConfigureAwait(false);
+ try
+ {
+ await client.CoreV1.ReadNamespaceAsync(name).ConfigureAwait(false);
+ }
+ catch (AggregateException ex)
+ {
+ foreach (var innerEx in ex.InnerExceptions)
+ {
+ if (innerEx is k8s.Autorest.HttpOperationException exception)
+ {
+ var code = exception.Response.StatusCode;
+ if (code == HttpStatusCode.NotFound)
+ {
+ return;
+ }
+
+ throw;
+ }
+ }
+ }
+ catch (k8s.Autorest.HttpOperationException ex)
+ {
+ if (ex.Response.StatusCode == HttpStatusCode.NotFound)
+ {
+ return;
+ }
+
+ throw;
+ }
+ }
+}
+
+void Delete(IKubernetes client, string name, int delayMillis)
+{
+ DeleteAsync(client, name, delayMillis).Wait();
+}
+
+var k8SClientConfig = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+IKubernetes client = new Kubernetes(k8SClientConfig);
+
+ListNamespaces(client);
+
+var ns = new V1Namespace { Metadata = new V1ObjectMeta { Name = "test" } };
+
+var result = client.CoreV1.CreateNamespace(ns);
+Console.WriteLine(result);
+
+ListNamespaces(client);
+
+var status = client.CoreV1.DeleteNamespace(ns.Metadata.Name, new V1DeleteOptions());
+
+if (status.HasObject)
+{
+ var obj = status.ObjectView();
+ Console.WriteLine(obj.Status.Phase);
+
+ Delete(client, ns.Metadata.Name, 3 * 1000);
+}
+else
+{
+ Console.WriteLine(status.Message);
+}
+
+ListNamespaces(client);
diff --git a/examples/namespace/namespace.csproj b/examples/namespace/namespace.csproj
index f35abd52b..f850aa91d 100644
--- a/examples/namespace/namespace.csproj
+++ b/examples/namespace/namespace.csproj
@@ -1,12 +1,7 @@
-
-
-
-
Exe
- netcoreapp2.0
diff --git a/examples/openTelemetryConsole/Program.cs b/examples/openTelemetryConsole/Program.cs
new file mode 100644
index 000000000..4b7406be3
--- /dev/null
+++ b/examples/openTelemetryConsole/Program.cs
@@ -0,0 +1,37 @@
+using k8s;
+using OpenTelemetry;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
+
+var serviceName = "MyCompany.MyProduct.MyService";
+var serviceVersion = "1.0.0";
+
+// Create the OpenTelemetry TraceProvide with HttpClient instrumentation enabled
+// NOTE: for this example telemetry will be exported to console
+using var tracerProvider = Sdk.CreateTracerProviderBuilder()
+ .AddSource(serviceName)
+ .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName: serviceName, serviceVersion: serviceVersion))
+ .AddHttpClientInstrumentation()
+ .AddConsoleExporter()
+ .Build();
+
+// Load kubernetes configuration
+var config = KubernetesClientConfiguration.BuildDefaultConfig();
+
+// Create an istance of Kubernetes client
+IKubernetes client = new Kubernetes(config);
+
+// Read the list of pods contained in default namespace
+var list = client.CoreV1.ListNamespacedPod("default");
+
+// Print the name of pods
+foreach (var item in list.Items)
+{
+ Console.WriteLine(item.Metadata.Name);
+}
+
+// Or empty if there are no pods
+if (list.Items.Count == 0)
+{
+ Console.WriteLine("Empty!");
+}
diff --git a/examples/openTelemetryConsole/openTelemetryConsole.csproj b/examples/openTelemetryConsole/openTelemetryConsole.csproj
new file mode 100644
index 000000000..ff48d4450
--- /dev/null
+++ b/examples/openTelemetryConsole/openTelemetryConsole.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/examples/patch-aot/Program.cs b/examples/patch-aot/Program.cs
new file mode 100644
index 000000000..e72f6a4d2
--- /dev/null
+++ b/examples/patch-aot/Program.cs
@@ -0,0 +1,33 @@
+using k8s;
+using k8s.Models;
+
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+IKubernetes client = new Kubernetes(config);
+Console.WriteLine("Starting Request!");
+
+var pod = client.CoreV1.ListNamespacedPod("default").Items.First();
+var name = pod.Metadata.Name;
+PrintLabels(pod);
+
+var patchStr = @"
+{
+ ""metadata"": {
+ ""labels"": {
+ ""test"": ""test""
+ }
+ }
+}";
+
+client.CoreV1.PatchNamespacedPod(new V1Patch(patchStr, V1Patch.PatchType.MergePatch), name, "default");
+PrintLabels(client.CoreV1.ReadNamespacedPod(name, "default"));
+
+static void PrintLabels(V1Pod pod)
+{
+ Console.WriteLine($"Labels: for {pod.Metadata.Name}");
+ foreach (var (k, v) in pod.Metadata.Labels)
+ {
+ Console.WriteLine($"{k} : {v}");
+ }
+
+ Console.WriteLine("=-=-=-=-=-=-=-=-=-=-=");
+}
diff --git a/examples/patch-aot/patch-aot.csproj b/examples/patch-aot/patch-aot.csproj
new file mode 100644
index 000000000..c2c806215
--- /dev/null
+++ b/examples/patch-aot/patch-aot.csproj
@@ -0,0 +1,11 @@
+
+
+ Exe
+ enable
+ enable
+ true
+
+
+
+
+
diff --git a/examples/patch/Program.cs b/examples/patch/Program.cs
new file mode 100644
index 000000000..f8cefa67c
--- /dev/null
+++ b/examples/patch/Program.cs
@@ -0,0 +1,35 @@
+using k8s;
+using k8s.Models;
+using System;
+using System.Linq;
+
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+IKubernetes client = new Kubernetes(config);
+Console.WriteLine("Starting Request!");
+
+var pod = client.CoreV1.ListNamespacedPod("default").Items.First();
+var name = pod.Metadata.Name;
+PrintLabels(pod);
+
+var patchStr = @"
+{
+ ""metadata"": {
+ ""labels"": {
+ ""test"": ""test""
+ }
+ }
+}";
+
+client.CoreV1.PatchNamespacedPod(new V1Patch(patchStr, V1Patch.PatchType.MergePatch), name, "default");
+PrintLabels(client.CoreV1.ReadNamespacedPod(name, "default"));
+
+void PrintLabels(V1Pod pod)
+{
+ Console.WriteLine($"Labels: for {pod.Metadata.Name}");
+ foreach (var (k, v) in pod.Metadata.Labels)
+ {
+ Console.WriteLine($"{k} : {v}");
+ }
+
+ Console.WriteLine("=-=-=-=-=-=-=-=-=-=-=");
+}
diff --git a/examples/patch/patch.csproj b/examples/patch/patch.csproj
new file mode 100644
index 000000000..f850aa91d
--- /dev/null
+++ b/examples/patch/patch.csproj
@@ -0,0 +1,7 @@
+
+
+
+ Exe
+
+
+
diff --git a/examples/portforward/PortForward.cs b/examples/portforward/PortForward.cs
new file mode 100644
index 000000000..ee095e073
--- /dev/null
+++ b/examples/portforward/PortForward.cs
@@ -0,0 +1,71 @@
+using k8s;
+using k8s.Models;
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading.Tasks;
+
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+IKubernetes client = new Kubernetes(config);
+Console.WriteLine("Starting port forward!");
+
+var list = client.CoreV1.ListNamespacedPod("default");
+var pod = list.Items[0];
+await Forward(client, pod).ConfigureAwait(false);
+
+async Task Forward(IKubernetes client, V1Pod pod)
+{
+ // Note this is single-threaded, it won't handle concurrent requests well...
+ var webSocket = await client.WebSocketNamespacedPodPortForwardAsync(pod.Metadata.Name, "default", new int[] { 80 }, "v4.channel.k8s.io").ConfigureAwait(false);
+ var demux = new StreamDemuxer(webSocket, StreamType.PortForward);
+ demux.Start();
+
+ var stream = demux.GetStream((byte?)0, (byte?)0);
+
+ IPAddress ipAddress = IPAddress.Loopback;
+ IPEndPoint localEndPoint = new IPEndPoint(ipAddress, 8080);
+ Socket listener = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
+ listener.Bind(localEndPoint);
+ listener.Listen(100);
+
+ Socket handler = null;
+
+ // Note this will only accept a single connection
+ var accept = Task.Run(() =>
+ {
+ while (true)
+ {
+ handler = listener.Accept();
+ var bytes = new byte[4096];
+ while (true)
+ {
+ int bytesRec = handler.Receive(bytes);
+ stream.Write(bytes, 0, bytesRec);
+ if (bytesRec == 0 || Encoding.ASCII.GetString(bytes, 0, bytesRec).IndexOf("") > -1)
+ {
+ break;
+ }
+ }
+ }
+ });
+
+ var copy = Task.Run(() =>
+ {
+ var buff = new byte[4096];
+ while (true)
+ {
+ var read = stream.Read(buff, 0, 4096);
+ handler.Send(buff, read, 0);
+ }
+ });
+
+ await accept.ConfigureAwait(false);
+ await copy.ConfigureAwait(false);
+ if (handler != null)
+ {
+ handler.Close();
+ }
+
+ listener.Close();
+}
diff --git a/examples/portforward/portforward.csproj b/examples/portforward/portforward.csproj
new file mode 100644
index 000000000..e3b6154bb
--- /dev/null
+++ b/examples/portforward/portforward.csproj
@@ -0,0 +1,7 @@
+
+
+
+ Exe
+
+
+
\ No newline at end of file
diff --git a/examples/resize/Program.cs b/examples/resize/Program.cs
new file mode 100644
index 000000000..85fbeb9b2
--- /dev/null
+++ b/examples/resize/Program.cs
@@ -0,0 +1,63 @@
+using k8s;
+using k8s.Models;
+using System;
+using System.Collections.Generic;
+
+
+var config = KubernetesClientConfiguration.BuildDefaultConfig();
+var client = new Kubernetes(config);
+
+
+var pod = new V1Pod
+{
+ Metadata = new V1ObjectMeta { Name = "nginx-pod" },
+ Spec = new V1PodSpec
+ {
+ Containers =
+ [
+ new V1Container
+ {
+ Name = "nginx",
+ Image = "nginx",
+ Resources = new V1ResourceRequirements
+ {
+ Requests = new Dictionary()
+ {
+ ["cpu"] = "100m",
+ },
+ },
+ },
+ ],
+ },
+};
+{
+ var created = await client.CoreV1.CreateNamespacedPodAsync(pod, "default").ConfigureAwait(false);
+ Console.WriteLine($"Created pod: {created.Metadata.Name}");
+}
+
+{
+ var patchStr = @"
+ {
+ ""spec"": {
+ ""containers"": [
+ {
+ ""name"": ""nginx"",
+ ""resources"": {
+ ""requests"": {
+ ""cpu"": ""200m""
+ }
+ }
+ }
+ ]
+ }
+ }";
+
+ var patch = await client.CoreV1.PatchNamespacedPodResizeAsync(new V1Patch(patchStr, V1Patch.PatchType.MergePatch), "nginx-pod", "default").ConfigureAwait(false);
+
+ if (patch?.Spec?.Containers?.Count > 0 &&
+ patch.Spec.Containers[0].Resources?.Requests != null &&
+ patch.Spec.Containers[0].Resources.Requests.TryGetValue("cpu", out var cpuQty))
+ {
+ Console.WriteLine($"CPU request: {cpuQty}");
+ }
+}
diff --git a/examples/resize/resize.csproj b/examples/resize/resize.csproj
new file mode 100644
index 000000000..d1e5b4724
--- /dev/null
+++ b/examples/resize/resize.csproj
@@ -0,0 +1,5 @@
+
+
+ Exe
+
+
\ No newline at end of file
diff --git a/examples/restart/Program.cs b/examples/restart/Program.cs
new file mode 100644
index 000000000..894e305a6
--- /dev/null
+++ b/examples/restart/Program.cs
@@ -0,0 +1,68 @@
+using Json.Patch;
+using k8s;
+using k8s.Models;
+using System.Text.Json;
+
+async Task RestartDaemonSetAsync(string name, string @namespace, IKubernetes client)
+{
+ var daemonSet = await client.AppsV1.ReadNamespacedDaemonSetAsync(name, @namespace).ConfigureAwait(false);
+ var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true };
+ var old = JsonSerializer.SerializeToDocument(daemonSet, options);
+ var now = DateTimeOffset.Now.ToUnixTimeSeconds();
+ var restart = new Dictionary
+ {
+ ["date"] = now.ToString(),
+ };
+
+ daemonSet.Spec.Template.Metadata.Annotations = restart;
+
+ var expected = JsonSerializer.SerializeToDocument(daemonSet);
+
+ var patch = old.CreatePatch(expected);
+ await client.AppsV1.PatchNamespacedDaemonSetAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace).ConfigureAwait(false);
+}
+
+async Task RestartDeploymentAsync(string name, string @namespace, IKubernetes client)
+{
+ var deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, @namespace).ConfigureAwait(false);
+ var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true };
+ var old = JsonSerializer.SerializeToDocument(deployment, options);
+ var now = DateTimeOffset.Now.ToUnixTimeSeconds();
+ var restart = new Dictionary
+ {
+ ["date"] = now.ToString(),
+ };
+
+ deployment.Spec.Template.Metadata.Annotations = restart;
+
+ var expected = JsonSerializer.SerializeToDocument(deployment);
+
+ var patch = old.CreatePatch(expected);
+ await client.AppsV1.PatchNamespacedDeploymentAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace).ConfigureAwait(false);
+}
+
+async Task RestartStatefulSetAsync(string name, string @namespace, IKubernetes client)
+{
+ var deployment = await client.AppsV1.ReadNamespacedStatefulSetAsync(name, @namespace).ConfigureAwait(false);
+ var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true };
+ var old = JsonSerializer.SerializeToDocument(deployment, options);
+ var now = DateTimeOffset.Now.ToUnixTimeSeconds();
+ var restart = new Dictionary
+ {
+ ["date"] = now.ToString(),
+ };
+
+ deployment.Spec.Template.Metadata.Annotations = restart;
+
+ var expected = JsonSerializer.SerializeToDocument(deployment);
+
+ var patch = old.CreatePatch(expected);
+ await client.AppsV1.PatchNamespacedStatefulSetAsync(new V1Patch(patch, V1Patch.PatchType.JsonPatch), name, @namespace).ConfigureAwait(false);
+}
+
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+IKubernetes client = new Kubernetes(config);
+
+await RestartDeploymentAsync("event-exporter", "monitoring", client).ConfigureAwait(false);
+await RestartDaemonSetAsync("prometheus-exporter", "monitoring", client).ConfigureAwait(false);
+await RestartStatefulSetAsync("argocd-application-controlle", "argocd", client).ConfigureAwait(false);
diff --git a/examples/restart/restart.csproj b/examples/restart/restart.csproj
new file mode 100644
index 000000000..0d5a49c4c
--- /dev/null
+++ b/examples/restart/restart.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/examples/simple/PodList.cs b/examples/simple/PodList.cs
index dd783c7a3..751622c16 100755
--- a/examples/simple/PodList.cs
+++ b/examples/simple/PodList.cs
@@ -1,25 +1,17 @@
-using System;
-using k8s;
-
-namespace simple
-{
- internal class PodList
- {
- private static void Main(string[] args)
- {
- var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
- IKubernetes client = new Kubernetes(config);
- Console.WriteLine("Starting Request!");
+using k8s;
+using System;
- var list = client.ListNamespacedPod("default");
- foreach (var item in list.Items)
- {
- Console.WriteLine(item.Metadata.Name);
- }
- if (list.Items.Count == 0)
- {
- Console.WriteLine("Empty!");
- }
- }
- }
-}
+var config = KubernetesClientConfiguration.BuildDefaultConfig();
+IKubernetes client = new Kubernetes(config);
+Console.WriteLine("Starting Request!");
+
+var list = client.CoreV1.ListNamespacedPod("default");
+foreach (var item in list.Items)
+{
+ Console.WriteLine(item.Metadata.Name);
+}
+
+if (list.Items.Count == 0)
+{
+ Console.WriteLine("Empty!");
+}
diff --git a/examples/simple/simple.csproj b/examples/simple/simple.csproj
index 270736f72..52e6553de 100755
--- a/examples/simple/simple.csproj
+++ b/examples/simple/simple.csproj
@@ -1,12 +1,7 @@
-
-
-
-
Exe
- netcoreapp2.0
diff --git a/examples/watch/Program.cs b/examples/watch/Program.cs
index 88514f77d..1aff65883 100644
--- a/examples/watch/Program.cs
+++ b/examples/watch/Program.cs
@@ -1,33 +1,39 @@
-using System;
-using System.Threading;
-using k8s;
-using k8s.Models;
-
-namespace watch
-{
- internal class Program
- {
- private static void Main(string[] args)
- {
- var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
-
- IKubernetes client = new Kubernetes(config);
-
- var podlistResp = client.ListNamespacedPodWithHttpMessagesAsync("default", watch: true).Result;
- using (podlistResp.Watch((type, item) =>
- {
- Console.WriteLine("==on watch event==");
- Console.WriteLine(type);
- Console.WriteLine(item.Metadata.Name);
- Console.WriteLine("==on watch event==");
- }))
- {
- Console.WriteLine("press ctrl + c to stop watching");
-
- var ctrlc = new ManualResetEventSlim(false);
- Console.CancelKeyPress += (sender, eventArgs) => ctrlc.Set();
- ctrlc.Wait();
- }
- }
- }
-}
+using k8s;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+var config = KubernetesClientConfiguration.BuildConfigFromConfigFile();
+
+IKubernetes client = new Kubernetes(config);
+
+var podlistResp = client.CoreV1.WatchListNamespacedPodAsync("default");
+
+// C# 8 required https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8
+await foreach (var (type, item) in podlistResp.ConfigureAwait(false))
+{
+ Console.WriteLine("==on watch event==");
+ Console.WriteLine(type);
+ Console.WriteLine(item.Metadata.Name);
+ Console.WriteLine("==on watch event==");
+}
+
+#pragma warning disable CS8321 // Remove unused private members
+void WatchUsingCallback(IKubernetes client)
+#pragma warning restore CS8321 // Remove unused private members
+{
+ using (var podlistResp = client.CoreV1.WatchListNamespacedPod("default", onEvent: (type, item) =>
+ {
+ Console.WriteLine("==on watch event==");
+ Console.WriteLine(type);
+ Console.WriteLine(item.Metadata.Name);
+ Console.WriteLine("==on watch event==");
+ }))
+ {
+ Console.WriteLine("press ctrl + c to stop watching");
+
+ var ctrlc = new ManualResetEventSlim(false);
+ Console.CancelKeyPress += (sender, eventArgs) => ctrlc.Set();
+ ctrlc.Wait();
+ }
+}
\ No newline at end of file
diff --git a/examples/watch/watch.csproj b/examples/watch/watch.csproj
index 698630e59..f850aa91d 100644
--- a/examples/watch/watch.csproj
+++ b/examples/watch/watch.csproj
@@ -2,11 +2,6 @@
Exe
- netcoreapp2.0
-
-
-
-
diff --git a/examples/webApiDependencyInjection/Controllers/ExampleDependencyInjectionOnConstructorController.cs b/examples/webApiDependencyInjection/Controllers/ExampleDependencyInjectionOnConstructorController.cs
new file mode 100644
index 000000000..6bff6df0d
--- /dev/null
+++ b/examples/webApiDependencyInjection/Controllers/ExampleDependencyInjectionOnConstructorController.cs
@@ -0,0 +1,36 @@
+using k8s;
+using Microsoft.AspNetCore.Mvc;
+
+namespace webApiDependencyInjection.Controllers
+{
+ [ApiController]
+ [Route("[controller]")]
+ public class ExampleDependencyInjectionOnConstructorController : ControllerBase
+ {
+ private readonly IKubernetes kubernetesClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Injects the Kubernetes client into the controller.
+ ///
+ /// The Kubernetes client to interact with the Kubernetes API.
+ public ExampleDependencyInjectionOnConstructorController(IKubernetes kubernetesClient)
+ {
+ this.kubernetesClient = kubernetesClient;
+ }
+
+ ///
+ /// Retrieves the names of all pods in the default namespace using the injected Kubernetes client.
+ ///
+ /// A collection of pod names in the default namespace.
+ [HttpGet]
+ public IEnumerable GetPods()
+ {
+ // Read the list of pods contained in the default namespace
+ var podList = this.kubernetesClient.CoreV1.ListNamespacedPod("default");
+
+ // Return names of pods
+ return podList.Items.Select(pod => pod.Metadata.Name);
+ }
+ }
+}
diff --git a/examples/webApiDependencyInjection/Controllers/ExampleDependencyInjectionOnMethodController.cs b/examples/webApiDependencyInjection/Controllers/ExampleDependencyInjectionOnMethodController.cs
new file mode 100644
index 000000000..84427f5e2
--- /dev/null
+++ b/examples/webApiDependencyInjection/Controllers/ExampleDependencyInjectionOnMethodController.cs
@@ -0,0 +1,27 @@
+using k8s;
+using Microsoft.AspNetCore.Mvc;
+
+namespace webApiDependencyInjection.Controllers
+{
+ [ApiController]
+ [Route("[controller]")]
+ public class ExampleDependencyInjectionOnMethodController : ControllerBase
+ {
+ ///
+ /// Example using the kubernetes client injected directly into the method ([FromServices] IKubernetes kubernetesClient).
+ ///
+ /// The Kubernetes client instance injected via dependency injection.
+ /// A collection of pod names in the default namespace.
+ [HttpGet]
+ public IEnumerable GetPods([FromServices] IKubernetes kubernetesClient)
+ {
+ ArgumentNullException.ThrowIfNull(kubernetesClient);
+
+ // Read the list of pods contained in default namespace
+ var podList = kubernetesClient.CoreV1.ListNamespacedPod("default");
+
+ // Return names of pods
+ return podList.Items.Select(pod => pod.Metadata.Name);
+ }
+ }
+}
diff --git a/examples/webApiDependencyInjection/Program.cs b/examples/webApiDependencyInjection/Program.cs
new file mode 100644
index 000000000..3ec631b65
--- /dev/null
+++ b/examples/webApiDependencyInjection/Program.cs
@@ -0,0 +1,34 @@
+using k8s;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Load kubernetes configuration
+var kubernetesClientConfig = KubernetesClientConfiguration.BuildDefaultConfig();
+
+// Register Kubernetes client interface as sigleton
+builder.Services.AddSingleton(_ => new Kubernetes(kubernetesClientConfig));
+
+// Add services to the container.
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+builder.Services.AddControllers();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+// Start the service
+app.Run();
+
+
+// Swagger ui can be accesse at: http://localhost:/swagger
diff --git a/examples/webApiDependencyInjection/appsettings.Development.json b/examples/webApiDependencyInjection/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/examples/webApiDependencyInjection/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/examples/webApiDependencyInjection/appsettings.json b/examples/webApiDependencyInjection/appsettings.json
new file mode 100644
index 000000000..10f68b8c8
--- /dev/null
+++ b/examples/webApiDependencyInjection/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/examples/webApiDependencyInjection/webApiDependencyInjection.csproj b/examples/webApiDependencyInjection/webApiDependencyInjection.csproj
new file mode 100644
index 000000000..23e466d7e
--- /dev/null
+++ b/examples/webApiDependencyInjection/webApiDependencyInjection.csproj
@@ -0,0 +1,12 @@
+
+
+
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/examples/workerServiceDependencyInjection/Program.cs b/examples/workerServiceDependencyInjection/Program.cs
new file mode 100644
index 000000000..a894a33fe
--- /dev/null
+++ b/examples/workerServiceDependencyInjection/Program.cs
@@ -0,0 +1,17 @@
+using k8s;
+using workerServiceDependencyInjection;
+
+IHost host = Host.CreateDefaultBuilder(args)
+ .ConfigureServices(services =>
+ {
+ // Load kubernetes configuration
+ var kubernetesClientConfig = KubernetesClientConfiguration.BuildDefaultConfig();
+
+ // Register Kubernetes client interface as sigleton
+ services.AddSingleton(_ => new Kubernetes(kubernetesClientConfig));
+
+ services.AddHostedService();
+ })
+ .Build();
+
+await host.RunAsync().ConfigureAwait(false);
diff --git a/examples/workerServiceDependencyInjection/Worker.cs b/examples/workerServiceDependencyInjection/Worker.cs
new file mode 100644
index 000000000..cb2f82386
--- /dev/null
+++ b/examples/workerServiceDependencyInjection/Worker.cs
@@ -0,0 +1,41 @@
+using k8s;
+
+namespace workerServiceDependencyInjection
+{
+ public class Worker : BackgroundService
+ {
+ private readonly ILogger logger;
+ private readonly IKubernetes kubernetesClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Inject in the constructor the IKubernetes interface.
+ ///
+ /// The logger instance used for logging information.
+ /// The Kubernetes client used to interact with the Kubernetes API.
+ public Worker(ILogger logger, IKubernetes kubernetesClient)
+ {
+ this.logger = logger;
+ this.kubernetesClient = kubernetesClient;
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
+
+ // Read the list of pods contained in default namespace
+ var podList = kubernetesClient.CoreV1.ListNamespacedPod("default");
+
+ // Print pods names
+ foreach (var pod in podList.Items)
+ {
+ Console.WriteLine(pod.Metadata.Name);
+ }
+
+ await Task.Delay(1000, stoppingToken).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/examples/workerServiceDependencyInjection/appsettings.Development.json b/examples/workerServiceDependencyInjection/appsettings.Development.json
new file mode 100644
index 000000000..b2dcdb674
--- /dev/null
+++ b/examples/workerServiceDependencyInjection/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/examples/workerServiceDependencyInjection/appsettings.json b/examples/workerServiceDependencyInjection/appsettings.json
new file mode 100644
index 000000000..b2dcdb674
--- /dev/null
+++ b/examples/workerServiceDependencyInjection/appsettings.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/examples/workerServiceDependencyInjection/workerServiceDependencyInjection.csproj b/examples/workerServiceDependencyInjection/workerServiceDependencyInjection.csproj
new file mode 100644
index 000000000..84522ab7c
--- /dev/null
+++ b/examples/workerServiceDependencyInjection/workerServiceDependencyInjection.csproj
@@ -0,0 +1,11 @@
+
+
+
+ enable
+ enable
+
+
+
+
+
+
diff --git a/examples/yaml/Program.cs b/examples/yaml/Program.cs
new file mode 100644
index 000000000..47b70bdfe
--- /dev/null
+++ b/examples/yaml/Program.cs
@@ -0,0 +1,18 @@
+using k8s;
+using k8s.Models;
+using System;
+using System.Collections.Generic;
+
+var typeMap = new Dictionary
+{
+ { "v1/Pod", typeof(V1Pod) },
+ { "v1/Service", typeof(V1Service) },
+ { "apps/v1/Deployment", typeof(V1Deployment) },
+};
+
+var objects = await KubernetesYaml.LoadAllFromFileAsync(args[0], typeMap).ConfigureAwait(false);
+
+foreach (var obj in objects)
+{
+ Console.WriteLine(obj);
+}
diff --git a/examples/yaml/yaml.csproj b/examples/yaml/yaml.csproj
new file mode 100644
index 000000000..d1e5b4724
--- /dev/null
+++ b/examples/yaml/yaml.csproj
@@ -0,0 +1,5 @@
+
+
+ Exe
+
+
\ No newline at end of file
diff --git a/global.json b/global.json
new file mode 100644
index 000000000..101665708
--- /dev/null
+++ b/global.json
@@ -0,0 +1,9 @@
+{
+ "sdk": {
+ "version": "8.0.100",
+ "rollForward": "latestMajor"
+ },
+ "msbuild-sdks": {
+ "Microsoft.Build.Traversal": "4.1.0"
+ }
+}
diff --git a/kubernetes-client.proj b/kubernetes-client.proj
new file mode 100644
index 000000000..9f634d328
--- /dev/null
+++ b/kubernetes-client.proj
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/kubernetes-client.ruleset b/kubernetes-client.ruleset
new file mode 100644
index 000000000..8baea16b9
--- /dev/null
+++ b/kubernetes-client.ruleset
@@ -0,0 +1,303 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kubernetes-client.sln b/kubernetes-client.sln
deleted file mode 100644
index 5c467acaf..000000000
--- a/kubernetes-client.sln
+++ /dev/null
@@ -1,28 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26430.16
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "tests", "tests\tests.csproj", "{F578EB59-8E44-4652-AF8D-03F03E8A8B37}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KubernetesClient", "src\KubernetesClient.csproj", "{CDDE69B1-A259-4DE3-8439-3AAD789B8F32}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {F578EB59-8E44-4652-AF8D-03F03E8A8B37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F578EB59-8E44-4652-AF8D-03F03E8A8B37}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F578EB59-8E44-4652-AF8D-03F03E8A8B37}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F578EB59-8E44-4652-AF8D-03F03E8A8B37}.Release|Any CPU.Build.0 = Release|Any CPU
- {CDDE69B1-A259-4DE3-8439-3AAD789B8F32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CDDE69B1-A259-4DE3-8439-3AAD789B8F32}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CDDE69B1-A259-4DE3-8439-3AAD789B8F32}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CDDE69B1-A259-4DE3-8439-3AAD789B8F32}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
-EndGlobal
diff --git a/logo.png b/logo.png
new file mode 100644
index 000000000..05fc5e1d2
Binary files /dev/null and b/logo.png differ
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 000000000..972f9afee
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/CertUtils.cs b/src/CertUtils.cs
deleted file mode 100644
index a98e62b8a..000000000
--- a/src/CertUtils.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-using System;
-using System.IO;
-using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using k8s.Exceptions;
-using Org.BouncyCastle.Crypto;
-using Org.BouncyCastle.Crypto.Parameters;
-using Org.BouncyCastle.OpenSsl;
-using Org.BouncyCastle.Pkcs;
-using Org.BouncyCastle.Security;
-using Org.BouncyCastle.X509;
-
-namespace k8s
-{
- public static class CertUtils
- {
- ///
- /// Load pem encoded cert file
- ///
- /// Path to pem encoded cert file
- /// x509 instance.
- public static X509Certificate2 LoadPemFileCert(string file)
- {
- var certdata = File.ReadAllText(file)
- .Replace("-----BEGIN CERTIFICATE-----", "")
- .Replace("-----END CERTIFICATE-----", "")
- .Replace("\r", "")
- .Replace("\n", "");
-
- return new X509Certificate2(Convert.FromBase64String(certdata));
- }
-
- ///
- /// Generates pfx from client configuration
- ///
- /// Kuberentes Client Configuration
- /// Generated Pfx Path
- public static X509Certificate2 GeneratePfx(KubernetesClientConfiguration config)
- {
- byte[] keyData = null;
- byte[] certData = null;
-
- if (!string.IsNullOrWhiteSpace(config.ClientCertificateKeyData))
- {
- keyData = Convert.FromBase64String(config.ClientCertificateKeyData);
- }
- if (!string.IsNullOrWhiteSpace(config.ClientKeyFilePath))
- {
- keyData = File.ReadAllBytes(config.ClientKeyFilePath);
- }
-
- if (keyData == null)
- {
- throw new KubeConfigException("certData is empty");
- }
-
- if (!string.IsNullOrWhiteSpace(config.ClientCertificateData))
- {
- certData = Convert.FromBase64String(config.ClientCertificateData);
- }
- if (!string.IsNullOrWhiteSpace(config.ClientCertificateFilePath))
- {
- certData = File.ReadAllBytes(config.ClientCertificateFilePath);
- }
-
- if (certData == null)
- {
- throw new KubeConfigException("certData is empty");
- }
-
- var cert = new X509CertificateParser().ReadCertificate(new MemoryStream(certData));
-
- object obj;
- using (var reader = new StreamReader(new MemoryStream(keyData)))
- {
- obj = new PemReader(reader).ReadObject();
- var key = obj as AsymmetricCipherKeyPair;
- if (key != null)
- {
- var cipherKey = key;
- obj = cipherKey.Private;
- }
- }
-
- var rsaKeyParams = (RsaPrivateCrtKeyParameters) obj;
-
- var store = new Pkcs12StoreBuilder().Build();
- store.SetKeyEntry("K8SKEY", new AsymmetricKeyEntry(rsaKeyParams), new[] {new X509CertificateEntry(cert)});
-
- using (var pkcs = new MemoryStream())
- {
- store.Save(pkcs, new char[0], new SecureRandom());
- return new X509Certificate2(pkcs.ToArray());
- }
- }
- }
-}
diff --git a/src/IKubernetes.WebSocket.cs b/src/IKubernetes.WebSocket.cs
deleted file mode 100644
index cf6ff2e92..000000000
--- a/src/IKubernetes.WebSocket.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using System.Collections.Generic;
-using System.Net.WebSockets;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace k8s
-{
- public partial interface IKubernetes
- {
- ///
- /// Executes a command in a pod.
- ///
- ///
- /// name of the Pod
- ///
- ///
- /// object name and auth scope, such as for teams and projects
- ///
- ///
- /// Command is the remote command to execute. argv array. Not executed within a
- /// shell.
- ///
- ///
- /// Container in which to execute the command. Defaults to only container if
- /// there is only one container in the pod.
- ///
- ///
- /// Redirect the standard error stream of the pod for this call. Defaults to
- /// .
- ///
- ///
- /// Redirect the standard input stream of the pod for this call. Defaults to
- /// .
- ///
- ///
- /// Redirect the standard output stream of the pod for this call. Defaults to
- /// .
- ///
- ///
- /// TTY if true indicates that a tty will be allocated for the exec call.
- /// Defaults to .
- ///
- ///
- /// Headers that will be added to request.
- ///
- ///
- /// The cancellation token.
- ///
- ///
- /// Thrown when a required parameter is null
- ///
- ///
- /// A which can be used to communicate with the process running in the pod.
- ///
- Task WebSocketNamespacedPodExecAsync(string name, string @namespace = "default", string command = "/bin/bash", string container = null, bool stderr = true, bool stdin = true, bool stdout = true, bool tty = true, Dictionary> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
-
- ///
- /// Start port forwarding one or more ports of a pod.
- ///
- ///
- /// The name of the Pod
- ///
- ///
- /// The object name and auth scope, such as for teams and projects
- ///
- ///
- /// List of ports to forward.
- ///
- ///
- /// The headers that will be added to request.
- ///
- ///
- /// The cancellation token.
- ///
- Task WebSocketNamespacedPodPortForwardAsync(string name, string @namespace, IEnumerable ports, Dictionary> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
-
- ///
- /// connect GET requests to attach of Pod
- ///
- ///
- /// name of the Pod
- ///
- ///
- /// object name and auth scope, such as for teams and projects
- ///
- ///
- /// The container in which to execute the command. Defaults to only container
- /// if there is only one container in the pod.
- ///
- ///
- /// Stderr if true indicates that stderr is to be redirected for the attach
- /// call. Defaults to true.
- ///
- ///
- /// Stdin if true, redirects the standard input stream of the pod for this
- /// call. Defaults to false.
- ///
- ///
- /// Stdout if true indicates that stdout is to be redirected for the attach
- /// call. Defaults to true.
- ///
- ///
- /// TTY if true indicates that a tty will be allocated for the attach call.
- /// This is passed through the container runtime so the tty is allocated on the
- /// worker node by the container runtime. Defaults to false.
- ///
- ///
- /// Headers that will be added to request.
- ///
- ///
- /// The cancellation token.
- ///
- ///
- /// Thrown when the operation returned an invalid status code
- ///
- ///
- /// Thrown when unable to deserialize the response
- ///
- ///
- /// Thrown when a required parameter is null
- ///
- ///
- /// Thrown when a required parameter is null
- ///
- ///
- /// A response object containing the response body and response headers.
- ///
- Task WebSocketNamespacedPodAttachAsync(string name, string @namespace, string container = default(string), bool stderr = true, bool stdin = false, bool stdout = true, bool tty = false, Dictionary> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken));
- }
-}
diff --git a/src/IntstrIntOrString.cs b/src/IntstrIntOrString.cs
deleted file mode 100644
index 6f634f1d0..000000000
--- a/src/IntstrIntOrString.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-using System;
-using Newtonsoft.Json;
-
-namespace k8s.Models
-{
- internal class IntOrStringConverter : JsonConverter
- {
- public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
- {
- var s = (value as IntstrIntOrString)?.Value;
-
- if (int.TryParse(s, out var intv))
- {
- serializer.Serialize(writer, intv);
- return;
- }
-
- serializer.Serialize(writer, s);
- }
-
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
- JsonSerializer serializer)
- {
- return (IntstrIntOrString) serializer.Deserialize(reader);
- }
-
- public override bool CanConvert(Type objectType)
- {
- return objectType == typeof(int) || objectType == typeof(string);
- }
- }
-
- [JsonConverter(typeof(IntOrStringConverter))]
- public partial class IntstrIntOrString
- {
- public static implicit operator int(IntstrIntOrString v)
- {
- return int.Parse(v.Value);
- }
-
- public static implicit operator IntstrIntOrString(int v)
- {
- return new IntstrIntOrString(Convert.ToString(v));
- }
-
- public static implicit operator string(IntstrIntOrString v)
- {
- return v.Value;
- }
-
- public static implicit operator IntstrIntOrString(string v)
- {
- return new IntstrIntOrString(v);
- }
-
- protected bool Equals(IntstrIntOrString other)
- {
- return string.Equals(Value, other.Value);
- }
-
- public override bool Equals(object obj)
- {
- if (ReferenceEquals(null, obj)) return false;
- if (ReferenceEquals(this, obj)) return true;
- if (obj.GetType() != this.GetType()) return false;
- return Equals((IntstrIntOrString) obj);
- }
-
- public override int GetHashCode()
- {
- return (Value != null ? Value.GetHashCode() : 0);
- }
- }
-}
diff --git a/src/KubeConfigModels/Cluster.cs b/src/KubeConfigModels/Cluster.cs
deleted file mode 100644
index 9cd6d565e..000000000
--- a/src/KubeConfigModels/Cluster.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace k8s.KubeConfigModels
-{
- using YamlDotNet.Serialization;
-
- public class Cluster
- {
- [YamlMember(Alias = "cluster")]
- public ClusterEndpoint ClusterEndpoint { get; set; }
-
- [YamlMember(Alias = "name")]
- public string Name { get; set; }
- }
-}
diff --git a/src/KubeConfigModels/ClusterEndpoint.cs b/src/KubeConfigModels/ClusterEndpoint.cs
deleted file mode 100644
index 7bc60ba16..000000000
--- a/src/KubeConfigModels/ClusterEndpoint.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-namespace k8s.KubeConfigModels
-{
- using YamlDotNet.Serialization;
-
- public class ClusterEndpoint
- {
- [YamlMember(Alias = "certificate-authority")]
- public string CertificateAuthority {get; set; }
-
- [YamlMember(Alias = "certificate-authority-data")]
- public string CertificateAuthorityData { get; set; }
-
- [YamlMember(Alias = "server")]
- public string Server { get; set; }
-
- [YamlMember(Alias = "insecure-skip-tls-verify")]
- public bool SkipTlsVerify { get; set; }
- }
-}
diff --git a/src/KubeConfigModels/Context.cs b/src/KubeConfigModels/Context.cs
deleted file mode 100644
index d921f2b72..000000000
--- a/src/KubeConfigModels/Context.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace k8s.KubeConfigModels
-{
- using YamlDotNet.Serialization;
-
- public class Context
- {
- [YamlMember(Alias = "context")]
- public ContextDetails ContextDetails { get; set; }
-
- [YamlMember(Alias = "name")]
- public string Name { get; set; }
-
- [YamlMember(Alias = "namespace")]
- public string Namespace { get; set; }
- }
-}
diff --git a/src/KubeConfigModels/ContextDetails.cs b/src/KubeConfigModels/ContextDetails.cs
deleted file mode 100644
index 291f587c3..000000000
--- a/src/KubeConfigModels/ContextDetails.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace k8s.KubeConfigModels
-{
- using YamlDotNet.RepresentationModel;
- using YamlDotNet.Serialization;
-
- public class ContextDetails
- {
- [YamlMember(Alias = "cluster")]
- public string Cluster { get; set; }
-
- [YamlMember(Alias = "user")]
- public string User { get; set; }
-
- [YamlMember(Alias = "namespace")]
- public string Namespace { get; set; }
- }
-}
diff --git a/src/KubeConfigModels/K8SConfiguration.cs b/src/KubeConfigModels/K8SConfiguration.cs
deleted file mode 100644
index 7d6184f96..000000000
--- a/src/KubeConfigModels/K8SConfiguration.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-namespace k8s.KubeConfigModels
-{
- using System.Collections.Generic;
- using YamlDotNet.Serialization;
-
- ///
- /// kubeconfig configuration model
- ///
- public class K8SConfiguration
- {
- [YamlMember(Alias = "preferences")]
- public IDictionary Preferences{ get; set; }
-
- [YamlMember(Alias = "apiVersion")]
- public string ApiVersion { get; set; }
-
- [YamlMember(Alias = "kind")]
- public string Kind { get; set; }
-
- [YamlMember(Alias = "current-context")]
- public string CurrentContext { get; set; }
-
- [YamlMember(Alias = "contexts")]
- public IEnumerable Contexts { get; set; } = new Context[0];
-
- [YamlMember(Alias = "clusters")]
- public IEnumerable Clusters { get; set; } = new Cluster[0];
-
- [YamlMember(Alias = "users")]
- public IEnumerable Users { get; set; } = new User[0];
- }
-}
diff --git a/src/KubeConfigModels/User.cs b/src/KubeConfigModels/User.cs
deleted file mode 100644
index 1c1290e05..000000000
--- a/src/KubeConfigModels/User.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace k8s.KubeConfigModels
-{
- using YamlDotNet.RepresentationModel;
- using YamlDotNet.Serialization;
-
- public class User
- {
- [YamlMember(Alias = "user")]
- public UserCredentials UserCredentials { get; set; }
-
- [YamlMember(Alias = "name")]
- public string Name { get; set; }
- }
-}
diff --git a/src/KubeConfigModels/UserCredentials.cs b/src/KubeConfigModels/UserCredentials.cs
deleted file mode 100644
index 2300de31d..000000000
--- a/src/KubeConfigModels/UserCredentials.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-namespace k8s.KubeConfigModels
-{
- using System.Collections.Generic;
- using YamlDotNet.RepresentationModel;
- using YamlDotNet.Serialization;
-
- public class UserCredentials
- {
- [YamlMember(Alias = "client-certificate-data")]
- public string ClientCertificateData { get; set; }
-
- [YamlMember(Alias = "client-certificate")]
- public string ClientCertificate { get; set; }
-
- [YamlMember(Alias = "client-key-data")]
- public string ClientKeyData { get; set; }
-
- [YamlMember(Alias = "client-key")]
- public string ClientKey { get; set; }
-
- [YamlMember(Alias = "token")]
- public string Token { get; set; }
-
- [YamlMember(Alias = "username")]
- public string UserName { get; set; }
-
- [YamlMember(Alias = "password")]
- public string Password { get; set; }
-
- [YamlMember(Alias = "auth-provider")]
- public Dictionary AuthProvider { get; set; }
- }
-}
diff --git a/src/Kubernetes.ConfigInit.cs b/src/Kubernetes.ConfigInit.cs
deleted file mode 100644
index a95a6e3e0..000000000
--- a/src/Kubernetes.ConfigInit.cs
+++ /dev/null
@@ -1,163 +0,0 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
-using System.Net.Http;
-using System.Net.Security;
-using System.Security.Cryptography.X509Certificates;
-using k8s.Exceptions;
-using k8s.Models;
-using Microsoft.Rest;
-
-namespace k8s
-{
- public partial class Kubernetes
- {
- ///
- /// Initializes a new instance of the class.
- ///
- ///
- /// Optional. The delegating handlers to add to the http client pipeline.
- ///
- ///
- /// Optional. The delegating handlers to add to the http client pipeline.
- ///
- public Kubernetes(KubernetesClientConfiguration config, params DelegatingHandler[] handlers) : this(handlers)
- {
- if (string.IsNullOrWhiteSpace(config.Host))
- {
- throw new KubeConfigException("Host url must be set");
- }
-
- try
- {
- BaseUri = new Uri(config.Host);
- }
- catch (UriFormatException e)
- {
- throw new KubeConfigException("Bad host url", e);
- }
-
- CaCert = config.SslCaCert;
-
- if (BaseUri.Scheme == "https")
- {
- if (config.SkipTlsVerify)
- {
- HttpClientHandler.ServerCertificateCustomValidationCallback =
- (sender, certificate, chain, sslPolicyErrors) => true;
- }
- else
- {
- if (CaCert == null)
- {
- throw new KubeConfigException("a CA must be set when SkipTlsVerify === false");
- }
-
- HttpClientHandler.ServerCertificateCustomValidationCallback = CertificateValidationCallBack;
- }
- }
-
- // set credentails for the kubernernet client
- SetCredentials(config, HttpClientHandler);
- }
-
- private X509Certificate2 CaCert { get; }
-
- partial void CustomInitialize()
- {
- AppendDelegatingHandler();
- DeserializationSettings.Converters.Add(new V1Status.V1StatusObjectViewConverter());
- }
-
- private void AppendDelegatingHandler() where T : DelegatingHandler, new()
- {
- var cur = FirstMessageHandler as DelegatingHandler;
-
- while (cur != null)
- {
- var next = cur.InnerHandler as DelegatingHandler;
-
- if (next == null)
- {
- // last one
- // append watcher handler between to last handler
- cur.InnerHandler = new T
- {
- InnerHandler = cur.InnerHandler
- };
- break;
- }
-
- cur = next;
- }
- }
-
- ///
- /// Set credentials for the Client
- ///
- /// k8s client configuration
- /// http client handler for the rest client
- /// Task
- private void SetCredentials(KubernetesClientConfiguration config, HttpClientHandler handler)
- {
- // set the Credentails for token based auth
- if (!string.IsNullOrWhiteSpace(config.AccessToken))
- {
- Credentials = new TokenCredentials(config.AccessToken);
- }
- else if (!string.IsNullOrWhiteSpace(config.Username) && !string.IsNullOrWhiteSpace(config.Password))
- {
- Credentials = new BasicAuthenticationCredentials
- {
- UserName = config.Username,
- Password = config.Password
- };
- }
- // othwerwise set handler for clinet cert based auth
- else if ((!string.IsNullOrWhiteSpace(config.ClientCertificateData) ||
- !string.IsNullOrWhiteSpace(config.ClientCertificateFilePath)) &&
- (!string.IsNullOrWhiteSpace(config.ClientCertificateKeyData) ||
- !string.IsNullOrWhiteSpace(config.ClientKeyFilePath)))
- {
- var cert = CertUtils.GeneratePfx(config);
-
- handler.ClientCertificates.Add(cert);
- }
- }
-
- ///
- /// SSl Cert Validation Callback
- ///
- /// sender
- /// client certificate
- /// chain
- /// ssl policy errors
- /// true if valid cert
- [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Unused by design")]
- private bool CertificateValidationCallBack(
- object sender,
- X509Certificate certificate,
- X509Chain chain,
- SslPolicyErrors sslPolicyErrors)
- {
- // If the certificate is a valid, signed certificate, return true.
- if (sslPolicyErrors == SslPolicyErrors.None)
- {
- return true;
- }
-
- // If there are errors in the certificate chain, look at each error to determine the cause.
- if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) != 0)
- {
- chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
-
- // add all your extra certificate chain
- chain.ChainPolicy.ExtraStore.Add(CaCert);
- chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
- var isValid = chain.Build((X509Certificate2) certificate);
- return isValid;
- }
- // In all other cases, return false.
- return false;
- }
- }
-}
diff --git a/src/Kubernetes.WebSocket.cs b/src/Kubernetes.WebSocket.cs
deleted file mode 100644
index 5149a1f91..000000000
--- a/src/Kubernetes.WebSocket.cs
+++ /dev/null
@@ -1,246 +0,0 @@
-using Microsoft.AspNetCore.WebUtilities;
-using Microsoft.Rest;
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Net.WebSockets;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace k8s
-{
- public partial class Kubernetes
- {
- ///
- /// Gets a function which returns a which will use to
- /// create a new connection to the Kubernetes cluster.
- ///
- public Func CreateWebSocketBuilder { get; set; } = () => new WebSocketBuilder();
-
- ///
- public Task WebSocketNamespacedPodExecAsync(string name, string @namespace = "default", string command = "/bin/sh", string container = null, bool stderr = true, bool stdin = true, bool stdout = true, bool tty = true, Dictionary> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
- {
- if (name == null)
- {
- throw new ArgumentNullException(nameof(name));
- }
-
- if (@namespace == null)
- {
- throw new ArgumentNullException(nameof(@namespace));
- }
-
- if (command == null)
- {
- throw new ArgumentNullException(nameof(command));
- }
-
- // Tracing
- bool _shouldTrace = ServiceClientTracing.IsEnabled;
- string _invocationId = null;
- if (_shouldTrace)
- {
- _invocationId = ServiceClientTracing.NextInvocationId.ToString();
- Dictionary tracingParameters = new Dictionary();
- tracingParameters.Add("command", command);
- tracingParameters.Add("container", container);
- tracingParameters.Add("name", name);
- tracingParameters.Add("namespace", @namespace);
- tracingParameters.Add("stderr", stderr);
- tracingParameters.Add("stdin", stdin);
- tracingParameters.Add("stdout", stdout);
- tracingParameters.Add("tty", tty);
- tracingParameters.Add("cancellationToken", cancellationToken);
- ServiceClientTracing.Enter(_invocationId, this, nameof(WebSocketNamespacedPodExecAsync), tracingParameters);
- }
-
- // Construct URL
- var uriBuilder = new UriBuilder(BaseUri);
- uriBuilder.Scheme = BaseUri.Scheme == "https" ? "wss" : "ws";
-
- if (!uriBuilder.Path.EndsWith("/"))
- {
- uriBuilder.Path += "/";
- }
-
- uriBuilder.Path += $"api/v1/namespaces/{@namespace}/pods/{name}/exec";
-
-
- uriBuilder.Query = QueryHelpers.AddQueryString(string.Empty, new Dictionary
- {
- { "command", command},
- { "container", container},
- { "stderr", stderr ? "1": "0"},
- { "stdin", stdin ? "1": "0"},
- { "stdout", stdout ? "1": "0"},
- { "tty", tty ? "1": "0"}
- });
-
- return this.StreamConnectAsync(uriBuilder.Uri, _invocationId, customHeaders, cancellationToken);
- }
-
- ///
- public Task WebSocketNamespacedPodPortForwardAsync(string name, string @namespace, IEnumerable ports, Dictionary> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
- {
- if (name == null)
- {
- throw new ArgumentNullException(nameof(name));
- }
-
- if (@namespace == null)
- {
- throw new ArgumentNullException(nameof(@namespace));
- }
-
- if (ports == null)
- {
- throw new ArgumentNullException(nameof(ports));
- }
-
- // Tracing
- bool _shouldTrace = ServiceClientTracing.IsEnabled;
- string _invocationId = null;
- if (_shouldTrace)
- {
- _invocationId = ServiceClientTracing.NextInvocationId.ToString();
- Dictionary tracingParameters = new Dictionary();
- tracingParameters.Add("name", name);
- tracingParameters.Add("@namespace", @namespace);
- tracingParameters.Add("ports", ports);
- tracingParameters.Add("cancellationToken", cancellationToken);
- ServiceClientTracing.Enter(_invocationId, this, nameof(WebSocketNamespacedPodPortForwardAsync), tracingParameters);
- }
-
- // Construct URL
- var uriBuilder = new UriBuilder(this.BaseUri);
- uriBuilder.Scheme = this.BaseUri.Scheme == "https" ? "wss" : "ws";
-
- if (!uriBuilder.Path.EndsWith("/"))
- {
- uriBuilder.Path += "/";
- }
-
- uriBuilder.Path += $"api/v1/namespaces/{@namespace}/pods/{name}/portforward";
-
- foreach (var port in ports)
- {
- uriBuilder.Query += $"ports={port}&";
- }
-
- return StreamConnectAsync(uriBuilder.Uri, _invocationId, customHeaders, cancellationToken);
- }
-
- ///
- public Task WebSocketNamespacedPodAttachAsync(string name, string @namespace, string container = default(string), bool stderr = true, bool stdin = false, bool stdout = true, bool tty = false, Dictionary> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
- {
- if (name == null)
- {
- throw new ArgumentNullException(nameof(name));
- }
-
- if (@namespace == null)
- {
- throw new ArgumentNullException(nameof(@namespace));
- }
-
- // Tracing
- bool _shouldTrace = ServiceClientTracing.IsEnabled;
- string _invocationId = null;
- if (_shouldTrace)
- {
- _invocationId = ServiceClientTracing.NextInvocationId.ToString();
- Dictionary tracingParameters = new Dictionary();
- tracingParameters.Add("container", container);
- tracingParameters.Add("name", name);
- tracingParameters.Add("namespace", @namespace);
- tracingParameters.Add("stderr", stderr);
- tracingParameters.Add("stdin", stdin);
- tracingParameters.Add("stdout", stdout);
- tracingParameters.Add("tty", tty);
- tracingParameters.Add("cancellationToken", cancellationToken);
- ServiceClientTracing.Enter(_invocationId, this, nameof(WebSocketNamespacedPodAttachAsync), tracingParameters);
- }
-
- // Construct URL
- var uriBuilder = new UriBuilder(this.BaseUri);
- uriBuilder.Scheme = this.BaseUri.Scheme == "https" ? "wss" : "ws";
-
- if (!uriBuilder.Path.EndsWith("/"))
- {
- uriBuilder.Path += "/";
- }
-
- uriBuilder.Path += $"api/v1/namespaces/{@namespace}/pods/{name}/portforward";
-
- uriBuilder.Query = QueryHelpers.AddQueryString(string.Empty, new Dictionary
- {
- { "container", container},
- { "stderr", stderr ? "1": "0"},
- { "stdin", stdin ? "1": "0"},
- { "stdout", stdout ? "1": "0"},
- { "tty", tty ? "1": "0"}
- });
-
- return StreamConnectAsync(uriBuilder.Uri, _invocationId, customHeaders, cancellationToken);
- }
-
- protected async Task StreamConnectAsync(Uri uri, string invocationId = null, Dictionary> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
- {
- bool _shouldTrace = ServiceClientTracing.IsEnabled;
-
- // Create WebSocket transport objects
- WebSocketBuilder webSocketBuilder = this.CreateWebSocketBuilder();
-
- // Set Headers
- if (customHeaders != null)
- {
- foreach (var _header in customHeaders)
- {
- webSocketBuilder.SetRequestHeader(_header.Key, string.Join(" ", _header.Value));
- }
- }
-
- // Set Credentials
- foreach (var cert in this.HttpClientHandler.ClientCertificates)
- {
- webSocketBuilder.AddClientCertificate(cert);
- }
-
- HttpRequestMessage message = new HttpRequestMessage();
- await this.Credentials.ProcessHttpRequestAsync(message, cancellationToken);
-
- foreach (var _header in message.Headers)
- {
- webSocketBuilder.SetRequestHeader(_header.Key, string.Join(" ", _header.Value));
- }
-
- // Send Request
- cancellationToken.ThrowIfCancellationRequested();
-
- WebSocket webSocket = null;
-
- try
- {
- webSocket = await webSocketBuilder.BuildAndConnectAsync(uri, CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- if (_shouldTrace)
- {
- ServiceClientTracing.Error(invocationId, ex);
- }
-
- throw;
- }
- finally
- {
- if (_shouldTrace)
- {
- ServiceClientTracing.Exit(invocationId, null);
- }
- }
-
- return webSocket;
- }
- }
-}
diff --git a/src/KubernetesClient.Aot/Global.cs b/src/KubernetesClient.Aot/Global.cs
new file mode 100644
index 000000000..2b5a4ae8e
--- /dev/null
+++ b/src/KubernetesClient.Aot/Global.cs
@@ -0,0 +1,10 @@
+global using k8s.Autorest;
+global using k8s.Models;
+global using System;
+global using System.Collections.Generic;
+global using System.IO;
+global using System.Linq;
+global using System.Text.Json;
+global using System.Text.Json.Serialization;
+global using System.Threading;
+global using System.Threading.Tasks;
diff --git a/src/KubernetesClient.Aot/KubeConfigModels/AuthProvider.cs b/src/KubernetesClient.Aot/KubeConfigModels/AuthProvider.cs
new file mode 100644
index 000000000..5bec9095e
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubeConfigModels/AuthProvider.cs
@@ -0,0 +1,23 @@
+using YamlDotNet.Serialization;
+
+namespace k8s.KubeConfigModels
+{
+ ///
+ /// Contains information that describes identity information. This is use to tell the kubernetes cluster who you are.
+ ///
+ [YamlSerializable]
+ public class AuthProvider
+ {
+ ///
+ /// Gets or sets the nickname for this auth provider.
+ ///
+ [YamlMember(Alias = "name")]
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets the configuration for this auth provider
+ ///
+ [YamlMember(Alias = "config")]
+ public Dictionary Config { get; set; }
+ }
+}
diff --git a/src/KubernetesClient.Aot/KubeConfigModels/Cluster.cs b/src/KubernetesClient.Aot/KubeConfigModels/Cluster.cs
new file mode 100644
index 000000000..80faf96a5
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubeConfigModels/Cluster.cs
@@ -0,0 +1,23 @@
+using YamlDotNet.Serialization;
+
+namespace k8s.KubeConfigModels
+{
+ ///
+ /// Relates nicknames to cluster information.
+ ///
+ [YamlSerializable]
+ public class Cluster
+ {
+ ///
+ /// Gets or sets the cluster information.
+ ///
+ [YamlMember(Alias = "cluster")]
+ public ClusterEndpoint ClusterEndpoint { get; set; }
+
+ ///
+ /// Gets or sets the nickname for this Cluster.
+ ///
+ [YamlMember(Alias = "name")]
+ public string Name { get; set; }
+ }
+}
diff --git a/src/KubernetesClient.Aot/KubeConfigModels/ClusterEndpoint.cs b/src/KubernetesClient.Aot/KubeConfigModels/ClusterEndpoint.cs
new file mode 100644
index 000000000..c40827651
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubeConfigModels/ClusterEndpoint.cs
@@ -0,0 +1,42 @@
+using YamlDotNet.Serialization;
+
+namespace k8s.KubeConfigModels
+{
+ ///
+ /// Contains information about how to communicate with a kubernetes cluster
+ ///
+ [YamlSerializable]
+ public class ClusterEndpoint
+ {
+ ///
+ /// Gets or sets the path to a cert file for the certificate authority.
+ ///
+ [YamlMember(Alias = "certificate-authority", ApplyNamingConventions = false)]
+ public string CertificateAuthority { get; set; }
+
+ ///
+ /// Gets or sets =PEM-encoded certificate authority certificates. Overrides .
+ ///
+ [YamlMember(Alias = "certificate-authority-data", ApplyNamingConventions = false)]
+ public string CertificateAuthorityData { get; set; }
+
+ ///
+ /// Gets or sets the address of the kubernetes cluster (https://hostname:port).
+ ///
+ [YamlMember(Alias = "server")]
+ public string Server { get; set; }
+
+ ///
+ /// Gets or sets a value to override the TLS server name.
+ ///
+ [YamlMember(Alias = "tls-server-name", ApplyNamingConventions = false)]
+ public string TlsServerName { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to skip the validity check for the server's certificate.
+ /// This will make your HTTPS connections insecure.
+ ///
+ [YamlMember(Alias = "insecure-skip-tls-verify", ApplyNamingConventions = false)]
+ public bool SkipTlsVerify { get; set; }
+ }
+}
diff --git a/src/KubernetesClient.Aot/KubeConfigModels/Context.cs b/src/KubernetesClient.Aot/KubeConfigModels/Context.cs
new file mode 100644
index 000000000..65241315f
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubeConfigModels/Context.cs
@@ -0,0 +1,23 @@
+using YamlDotNet.Serialization;
+
+namespace k8s.KubeConfigModels
+{
+ ///
+ /// Relates nicknames to context information.
+ ///
+ [YamlSerializable]
+ public class Context
+ {
+ ///
+ /// Gets or sets the context information.
+ ///
+ [YamlMember(Alias = "context")]
+ public ContextDetails ContextDetails { get; set; }
+
+ ///
+ /// Gets or sets the nickname for this context.
+ ///
+ [YamlMember(Alias = "name")]
+ public string Name { get; set; }
+ }
+}
diff --git a/src/KubernetesClient.Aot/KubeConfigModels/ContextDetails.cs b/src/KubernetesClient.Aot/KubeConfigModels/ContextDetails.cs
new file mode 100644
index 000000000..ca2bf1e07
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubeConfigModels/ContextDetails.cs
@@ -0,0 +1,30 @@
+using YamlDotNet.Serialization;
+
+namespace k8s.KubeConfigModels
+{
+ ///
+ /// Represents a tuple of references to a cluster (how do I communicate with a kubernetes cluster),
+ /// a user (how do I identify myself), and a namespace (what subset of resources do I want to work with)
+ ///
+ [YamlSerializable]
+ public class ContextDetails
+ {
+ ///
+ /// Gets or sets the name of the cluster for this context.
+ ///
+ [YamlMember(Alias = "cluster")]
+ public string Cluster { get; set; }
+
+ ///
+ /// Gets or sets the name of the user for this context.
+ ///
+ [YamlMember(Alias = "user")]
+ public string User { get; set; }
+
+ ///
+ /// /Gets or sets the default namespace to use on unspecified requests.
+ ///
+ [YamlMember(Alias = "namespace")]
+ public string Namespace { get; set; }
+ }
+}
diff --git a/src/KubernetesClient.Aot/KubeConfigModels/ExecCredentialResponse.cs b/src/KubernetesClient.Aot/KubeConfigModels/ExecCredentialResponse.cs
new file mode 100644
index 000000000..d593ff4f6
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubeConfigModels/ExecCredentialResponse.cs
@@ -0,0 +1,35 @@
+using YamlDotNet.Serialization;
+
+namespace k8s.KubeConfigModels
+{
+ [YamlSerializable]
+ public class ExecCredentialResponse
+ {
+ public class ExecStatus
+ {
+#nullable enable
+ [JsonPropertyName("expirationTimestamp")]
+ public DateTime? ExpirationTimestamp { get; set; }
+ [JsonPropertyName("token")]
+ public string? Token { get; set; }
+ [JsonPropertyName("clientCertificateData")]
+ public string? ClientCertificateData { get; set; }
+ [JsonPropertyName("clientKeyData")]
+ public string? ClientKeyData { get; set; }
+#nullable disable
+
+ public bool IsValid()
+ {
+ return !string.IsNullOrEmpty(Token) ||
+ (!string.IsNullOrEmpty(ClientCertificateData) && !string.IsNullOrEmpty(ClientKeyData));
+ }
+ }
+
+ [JsonPropertyName("apiVersion")]
+ public string ApiVersion { get; set; }
+ [JsonPropertyName("kind")]
+ public string Kind { get; set; }
+ [JsonPropertyName("status")]
+ public ExecStatus Status { get; set; }
+ }
+}
diff --git a/src/KubernetesClient.Aot/KubeConfigModels/ExecCredentialResponseContext.cs b/src/KubernetesClient.Aot/KubeConfigModels/ExecCredentialResponseContext.cs
new file mode 100644
index 000000000..c7ffd8294
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubeConfigModels/ExecCredentialResponseContext.cs
@@ -0,0 +1,7 @@
+namespace k8s.KubeConfigModels
+{
+ [JsonSerializable(typeof(ExecCredentialResponse))]
+ internal partial class ExecCredentialResponseContext : JsonSerializerContext
+ {
+ }
+}
diff --git a/src/KubernetesClient.Aot/KubeConfigModels/ExternalExecution.cs b/src/KubernetesClient.Aot/KubeConfigModels/ExternalExecution.cs
new file mode 100644
index 000000000..7e18f449e
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubeConfigModels/ExternalExecution.cs
@@ -0,0 +1,42 @@
+using YamlDotNet.Serialization;
+
+namespace k8s.KubeConfigModels
+{
+ [YamlSerializable]
+ public class ExternalExecution
+ {
+ [YamlMember(Alias = "apiVersion")]
+ public string ApiVersion { get; set; }
+
+ ///
+ /// The command to execute. Required.
+ ///
+ [YamlMember(Alias = "command")]
+ public string Command { get; set; }
+
+ ///
+ /// Environment variables to set when executing the plugin. Optional.
+ ///
+ [YamlMember(Alias = "env")]
+ public IList> EnvironmentVariables { get; set; }
+
+ ///
+ /// Arguments to pass when executing the plugin. Optional.
+ ///
+ [YamlMember(Alias = "args")]
+ public IList Arguments { get; set; }
+
+ ///
+ /// Text shown to the user when the executable doesn't seem to be present. Optional.
+ ///
+ [YamlMember(Alias = "installHint")]
+ public string InstallHint { get; set; }
+
+ ///
+ /// Whether or not to provide cluster information to this exec plugin as a part of
+ /// the KUBERNETES_EXEC_INFO environment variable. Optional.
+ ///
+ [YamlMember(Alias = "provideClusterInfo")]
+ public bool ProvideClusterInfo { get; set; }
+ }
+}
diff --git a/src/KubernetesClient.Aot/KubeConfigModels/K8SConfiguration.cs b/src/KubernetesClient.Aot/KubeConfigModels/K8SConfiguration.cs
new file mode 100644
index 000000000..a0c3a4fef
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubeConfigModels/K8SConfiguration.cs
@@ -0,0 +1,65 @@
+using YamlDotNet.Serialization;
+
+namespace k8s.KubeConfigModels
+{
+ ///
+ /// kubeconfig configuration model. Holds the information needed to build connect to remote
+ /// Kubernetes clusters as a given user.
+ ///
+ ///
+ /// Should be kept in sync with https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go
+ /// Should update MergeKubeConfig in KubernetesClientConfiguration.ConfigFile.cs if updated.
+ ///
+ [YamlSerializable]
+ public class K8SConfiguration
+ {
+ // ///
+ // /// Gets or sets general information to be use for CLI interactions
+ // ///
+ // [YamlMember(Alias = "preferences")]
+ // public IDictionary Preferences { get; set; }
+
+ [YamlMember(Alias = "apiVersion")]
+ public string ApiVersion { get; set; }
+
+ [YamlMember(Alias = "kind")]
+ public string Kind { get; set; }
+
+ ///
+ /// Gets or sets the name of the context that you would like to use by default.
+ ///
+ [YamlMember(Alias = "current-context", ApplyNamingConventions = false)]
+ public string CurrentContext { get; set; }
+
+ ///
+ /// Gets or sets a map of referencable names to context configs.
+ ///
+ [YamlMember(Alias = "contexts")]
+ public List Contexts { get; set; } = new List();
+
+ ///
+ /// Gets or sets a map of referencable names to cluster configs.
+ ///
+ [YamlMember(Alias = "clusters")]
+ public List Clusters { get; set; } = new List();
+
+ ///
+ /// Gets or sets a map of referencable names to user configs
+ ///
+ [YamlMember(Alias = "users")]
+ public List Users { get; set; } = new List();
+
+ // ///
+ // /// Gets or sets additional information. This is useful for extenders so that reads and writes don't clobber unknown fields.
+ // ///
+ // [YamlMember(Alias = "extensions")]
+ // public List Extensions { get; set; }
+
+ ///
+ /// Gets or sets the name of the Kubernetes configuration file. This property is set only when the configuration
+ /// was loaded from disk, and can be used to resolve relative paths.
+ ///
+ [YamlIgnore]
+ public string FileName { get; set; }
+ }
+}
diff --git a/src/KubernetesClient.Aot/KubeConfigModels/StaticContext.cs b/src/KubernetesClient.Aot/KubeConfigModels/StaticContext.cs
new file mode 100644
index 000000000..ae9be922e
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubeConfigModels/StaticContext.cs
@@ -0,0 +1,8 @@
+using YamlDotNet.Serialization;
+
+namespace k8s.KubeConfigModels;
+
+[YamlStaticContext]
+public partial class StaticContext : YamlDotNet.Serialization.StaticContext
+{
+}
\ No newline at end of file
diff --git a/src/KubernetesClient.Aot/KubeConfigModels/User.cs b/src/KubernetesClient.Aot/KubeConfigModels/User.cs
new file mode 100644
index 000000000..557f02256
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubeConfigModels/User.cs
@@ -0,0 +1,23 @@
+using YamlDotNet.Serialization;
+
+namespace k8s.KubeConfigModels
+{
+ ///
+ /// Relates nicknames to auth information.
+ ///
+ [YamlSerializable]
+ public class User
+ {
+ ///
+ /// Gets or sets the auth information.
+ ///
+ [YamlMember(Alias = "user")]
+ public UserCredentials UserCredentials { get; set; }
+
+ ///
+ /// Gets or sets the nickname for this auth information.
+ ///
+ [YamlMember(Alias = "name")]
+ public string Name { get; set; }
+ }
+}
diff --git a/src/KubernetesClient.Aot/KubeConfigModels/UserCredentials.cs b/src/KubernetesClient.Aot/KubeConfigModels/UserCredentials.cs
new file mode 100644
index 000000000..bd8a5063e
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubeConfigModels/UserCredentials.cs
@@ -0,0 +1,83 @@
+using YamlDotNet.Serialization;
+
+namespace k8s.KubeConfigModels
+{
+ ///
+ /// Contains information that describes identity information. This is use to tell the kubernetes cluster who you are.
+ ///
+ [YamlSerializable]
+ public class UserCredentials
+ {
+ ///
+ /// Gets or sets PEM-encoded data from a client cert file for TLS. Overrides .
+ ///
+ [YamlMember(Alias = "client-certificate-data", ApplyNamingConventions = false)]
+ public string ClientCertificateData { get; set; }
+
+ ///
+ /// Gets or sets the path to a client cert file for TLS.
+ ///
+ [YamlMember(Alias = "client-certificate", ApplyNamingConventions = false)]
+ public string ClientCertificate { get; set; }
+
+ ///
+ /// Gets or sets PEM-encoded data from a client key file for TLS. Overrides .
+ ///
+ [YamlMember(Alias = "client-key-data", ApplyNamingConventions = false)]
+ public string ClientKeyData { get; set; }
+
+ ///
+ /// Gets or sets the path to a client key file for TLS.
+ ///
+ [YamlMember(Alias = "client-key", ApplyNamingConventions = false)]
+ public string ClientKey { get; set; }
+
+ ///
+ /// Gets or sets the bearer token for authentication to the kubernetes cluster.
+ ///
+ [YamlMember(Alias = "token")]
+ public string Token { get; set; }
+
+ ///
+ /// Gets or sets the username to impersonate. The name matches the flag.
+ ///
+ [YamlMember(Alias = "as")]
+ public string Impersonate { get; set; }
+
+ ///
+ /// Gets or sets the groups to impersonate.
+ ///
+ [YamlMember(Alias = "as-groups", ApplyNamingConventions = false)]
+ public IEnumerable ImpersonateGroups { get; set; } = new string[0];
+
+ ///
+ /// Gets or sets additional information for impersonated user.
+ ///
+ [YamlMember(Alias = "as-user-extra", ApplyNamingConventions = false)]
+ public Dictionary ImpersonateUserExtra { get; set; } = new Dictionary();
+
+ ///
+ /// Gets or sets the username for basic authentication to the kubernetes cluster.
+ ///
+ [YamlMember(Alias = "username")]
+ public string UserName { get; set; }
+
+ ///
+ /// Gets or sets the password for basic authentication to the kubernetes cluster.
+ ///
+ [YamlMember(Alias = "password")]
+ public string Password { get; set; }
+
+ ///
+ /// Gets or sets custom authentication plugin for the kubernetes cluster.
+ ///
+ [YamlMember(Alias = "auth-provider", ApplyNamingConventions = false)]
+ public AuthProvider AuthProvider { get; set; }
+
+ ///
+ /// Gets or sets external command and its arguments to receive user credentials
+ ///
+ [YamlMember(Alias = "exec")]
+ public ExternalExecution ExternalExecution { get; set; }
+ }
+}
diff --git a/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj b/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj
new file mode 100644
index 000000000..5c7cf8fed
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubernetesClient.Aot.csproj
@@ -0,0 +1,113 @@
+
+
+
+ net8.0;net9.0
+ k8s
+ true
+ true
+ true
+ $(DefineConstants);K8S_AOT
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs b/src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs
new file mode 100644
index 000000000..a2301b464
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubernetesClientConfiguration.ConfigFile.cs
@@ -0,0 +1,774 @@
+using k8s.Authentication;
+using k8s.Exceptions;
+using k8s.KubeConfigModels;
+using System.Diagnostics;
+using System.Net;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography.X509Certificates;
+
+namespace k8s
+{
+ public partial class KubernetesClientConfiguration
+ {
+ ///
+ /// kubeconfig Default Location
+ ///
+ public static readonly string KubeConfigDefaultLocation =
+ RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE") ?? @"\", @".kube\config")
+ : Path.Combine(Environment.GetEnvironmentVariable("HOME") ?? "/", ".kube/config");
+
+ ///
+ /// Gets CurrentContext
+ ///
+ public string CurrentContext { get; private set; }
+
+ // For testing
+ internal static string KubeConfigEnvironmentVariable { get; set; } = "KUBECONFIG";
+
+ ///
+ /// Exec process timeout
+ ///
+ public static TimeSpan ExecTimeout { get; set; } = TimeSpan.FromMinutes(2);
+
+ ///
+ /// Exec process Standard Errors
+ ///
+ public static event EventHandler ExecStdError;
+
+ ///
+ /// Initializes a new instance of the from default locations
+ /// If the KUBECONFIG environment variable is set, then that will be used.
+ /// Next, it looks for a config file at .
+ /// Then, it checks whether it is executing inside a cluster and will use .
+ /// Finally, if nothing else exists, it creates a default config with localhost:8080 as host.
+ ///
+ ///
+ /// If multiple kubeconfig files are specified in the KUBECONFIG environment variable,
+ /// merges the files, where first occurrence wins. See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files.
+ ///
+ /// Instance of the class
+ public static KubernetesClientConfiguration BuildDefaultConfig()
+ {
+ var kubeconfig = Environment.GetEnvironmentVariable(KubeConfigEnvironmentVariable);
+ if (kubeconfig != null)
+ {
+ var configList = kubeconfig.Split(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ';' : ':')
+ .Select((s) => new FileInfo(s.Trim('"')));
+ var k8sConfig = LoadKubeConfig(configList.ToArray());
+ return BuildConfigFromConfigObject(k8sConfig);
+ }
+
+ if (File.Exists(KubeConfigDefaultLocation))
+ {
+ return BuildConfigFromConfigFile(KubeConfigDefaultLocation);
+ }
+
+ if (IsInCluster())
+ {
+ return InClusterConfig();
+ }
+
+ var config = new KubernetesClientConfiguration
+ {
+ Host = "http://localhost:8080/",
+ };
+
+ return config;
+ }
+
+ ///
+ /// Initializes a new instance of the from config file
+ ///
+ /// Explicit file path to kubeconfig. Set to null to use the default file path
+ /// override the context in config file, set null if do not want to override
+ /// kube api server endpoint
+ /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
+ /// file is located. When , the paths will be considered to be relative to the current working directory.
+ /// Instance of the class
+ public static KubernetesClientConfiguration BuildConfigFromConfigFile(
+ string kubeconfigPath = null,
+ string currentContext = null, string masterUrl = null, bool useRelativePaths = true)
+ {
+ return BuildConfigFromConfigFile(new FileInfo(kubeconfigPath ?? KubeConfigDefaultLocation), currentContext,
+ masterUrl, useRelativePaths);
+ }
+
+ ///
+ /// Initializes a new instance of the from config file
+ ///
+ /// Fileinfo of the kubeconfig, cannot be null
+ /// override the context in config file, set null if do not want to override
+ /// override the kube api server endpoint, set null if do not want to override
+ /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
+ /// file is located. When , the paths will be considered to be relative to the current working directory.
+ /// Instance of the class
+ public static KubernetesClientConfiguration BuildConfigFromConfigFile(
+ FileInfo kubeconfig,
+ string currentContext = null, string masterUrl = null, bool useRelativePaths = true)
+ {
+ return BuildConfigFromConfigFileAsync(kubeconfig, currentContext, masterUrl, useRelativePaths).GetAwaiter()
+ .GetResult();
+ }
+
+ ///
+ /// Initializes a new instance of the from config file
+ ///
+ /// Fileinfo of the kubeconfig, cannot be null
+ /// override the context in config file, set null if do not want to override
+ /// override the kube api server endpoint, set null if do not want to override
+ /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
+ /// file is located. When , the paths will be considered to be relative to the current working directory.
+ /// Instance of the class
+ public static async Task BuildConfigFromConfigFileAsync(
+ FileInfo kubeconfig,
+ string currentContext = null, string masterUrl = null, bool useRelativePaths = true)
+ {
+ if (kubeconfig == null)
+ {
+ throw new NullReferenceException(nameof(kubeconfig));
+ }
+
+ var k8SConfig = await LoadKubeConfigAsync(kubeconfig, useRelativePaths).ConfigureAwait(false);
+ var k8SConfiguration = GetKubernetesClientConfiguration(currentContext, masterUrl, k8SConfig);
+
+ return k8SConfiguration;
+ }
+
+ ///
+ /// Initializes a new instance of the from config file
+ ///
+ /// Stream of the kubeconfig, cannot be null
+ /// Override the current context in config, set null if do not want to override
+ /// Override the Kubernetes API server endpoint, set null if do not want to override
+ /// Instance of the class
+ public static KubernetesClientConfiguration BuildConfigFromConfigFile(
+ Stream kubeconfig,
+ string currentContext = null, string masterUrl = null)
+ {
+ return BuildConfigFromConfigFileAsync(kubeconfig, currentContext, masterUrl).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Initializes a new instance of the from config file
+ ///
+ /// Stream of the kubeconfig, cannot be null
+ /// Override the current context in config, set null if do not want to override
+ /// Override the Kubernetes API server endpoint, set null if do not want to override
+ /// Instance of the class
+ public static async Task BuildConfigFromConfigFileAsync(
+ Stream kubeconfig,
+ string currentContext = null, string masterUrl = null)
+ {
+ if (kubeconfig == null)
+ {
+ throw new NullReferenceException(nameof(kubeconfig));
+ }
+
+ if (!kubeconfig.CanSeek)
+ {
+ throw new Exception("Stream don't support seeking!");
+ }
+
+ kubeconfig.Position = 0;
+
+ var k8SConfig = await KubernetesYaml.LoadFromStreamAsync(kubeconfig).ConfigureAwait(false);
+ var k8SConfiguration = GetKubernetesClientConfiguration(currentContext, masterUrl, k8SConfig);
+
+ return k8SConfiguration;
+ }
+
+ ///
+ /// Initializes a new instance of from pre-loaded config object.
+ ///
+ /// A , for example loaded from
+ /// Override the current context in config, set null if do not want to override
+ /// Override the Kubernetes API server endpoint, set null if do not want to override
+ /// Instance of the class
+ public static KubernetesClientConfiguration BuildConfigFromConfigObject(
+ K8SConfiguration k8SConfig,
+ string currentContext = null, string masterUrl = null)
+ => GetKubernetesClientConfiguration(currentContext, masterUrl, k8SConfig);
+
+ private static KubernetesClientConfiguration GetKubernetesClientConfiguration(
+ string currentContext,
+ string masterUrl, K8SConfiguration k8SConfig)
+ {
+ if (k8SConfig == null)
+ {
+ throw new ArgumentNullException(nameof(k8SConfig));
+ }
+
+ var k8SConfiguration = new KubernetesClientConfiguration();
+
+ currentContext = currentContext ?? k8SConfig.CurrentContext;
+ // only init context if context is set
+ if (currentContext != null)
+ {
+ k8SConfiguration.InitializeContext(k8SConfig, currentContext);
+ }
+
+ if (!string.IsNullOrWhiteSpace(masterUrl))
+ {
+ k8SConfiguration.Host = masterUrl;
+ }
+
+ if (string.IsNullOrWhiteSpace(k8SConfiguration.Host))
+ {
+ throw new KubeConfigException("Cannot infer server host url either from context or masterUrl");
+ }
+
+ return k8SConfiguration;
+ }
+
+ ///
+ /// Validates and Initializes Client Configuration
+ ///
+ /// Kubernetes Configuration
+ /// Current Context
+ private void InitializeContext(K8SConfiguration k8SConfig, string currentContext)
+ {
+ // current context
+ var activeContext =
+ k8SConfig.Contexts.FirstOrDefault(
+ c => c.Name.Equals(currentContext, StringComparison.OrdinalIgnoreCase));
+ if (activeContext == null)
+ {
+ throw new KubeConfigException($"CurrentContext: {currentContext} not found in contexts in kubeconfig");
+ }
+
+ if (string.IsNullOrEmpty(activeContext.ContextDetails?.Cluster))
+ {
+ // This serves as validation for any of the properties of ContextDetails being set.
+ // Other locations in code assume that ContextDetails is non-null.
+ throw new KubeConfigException($"Cluster not set for context `{currentContext}` in kubeconfig");
+ }
+
+ CurrentContext = activeContext.Name;
+
+ // cluster
+ SetClusterDetails(k8SConfig, activeContext);
+
+ // user
+ SetUserDetails(k8SConfig, activeContext);
+
+ // namespace
+ Namespace = activeContext.ContextDetails?.Namespace;
+ }
+
+ private void SetClusterDetails(K8SConfiguration k8SConfig, Context activeContext)
+ {
+ var clusterDetails =
+ k8SConfig.Clusters.FirstOrDefault(c => c.Name.Equals(
+ activeContext.ContextDetails.Cluster,
+ StringComparison.OrdinalIgnoreCase));
+
+ if (clusterDetails?.ClusterEndpoint == null)
+ {
+ throw new KubeConfigException($"Cluster not found for context `{activeContext}` in kubeconfig");
+ }
+
+ if (string.IsNullOrWhiteSpace(clusterDetails.ClusterEndpoint.Server))
+ {
+ throw new KubeConfigException($"Server not found for current-context `{activeContext}` in kubeconfig");
+ }
+
+ Host = clusterDetails.ClusterEndpoint.Server;
+ SkipTlsVerify = clusterDetails.ClusterEndpoint.SkipTlsVerify;
+ TlsServerName = clusterDetails.ClusterEndpoint.TlsServerName;
+
+ if (!Uri.TryCreate(Host, UriKind.Absolute, out var uri))
+ {
+ throw new KubeConfigException($"Bad server host URL `{Host}` (cannot be parsed)");
+ }
+
+ if (IPAddress.TryParse(uri.Host, out var ipAddress))
+ {
+ if (IPAddress.Equals(IPAddress.Any, ipAddress))
+ {
+ var builder = new UriBuilder(Host)
+ {
+ Host = $"{IPAddress.Loopback}",
+ };
+ Host = builder.ToString();
+ }
+ else if (IPAddress.Equals(IPAddress.IPv6Any, ipAddress))
+ {
+ var builder = new UriBuilder(Host)
+ {
+ Host = $"{IPAddress.IPv6Loopback}",
+ };
+ Host = builder.ToString();
+ }
+ }
+
+ if (uri.Scheme == "https")
+ {
+ if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthorityData))
+ {
+ var data = clusterDetails.ClusterEndpoint.CertificateAuthorityData;
+#if NET9_0_OR_GREATER
+ SslCaCerts = new X509Certificate2Collection(X509CertificateLoader.LoadCertificate(Convert.FromBase64String(data)));
+#else
+ string nullPassword = null;
+ // This null password is to change the constructor to fix this KB:
+ // https://support.microsoft.com/en-us/topic/kb5025823-change-in-how-net-applications-import-x-509-certificates-bf81c936-af2b-446e-9f7a-016f4713b46b
+ SslCaCerts = new X509Certificate2Collection(new X509Certificate2(Convert.FromBase64String(data), nullPassword));
+#endif
+ }
+ else if (!string.IsNullOrEmpty(clusterDetails.ClusterEndpoint.CertificateAuthority))
+ {
+#if NET9_0_OR_GREATER
+ SslCaCerts = new X509Certificate2Collection(X509CertificateLoader.LoadCertificateFromFile(GetFullPath(
+ k8SConfig,
+ clusterDetails.ClusterEndpoint.CertificateAuthority)));
+#else
+ SslCaCerts = new X509Certificate2Collection(new X509Certificate2(GetFullPath(
+ k8SConfig,
+ clusterDetails.ClusterEndpoint.CertificateAuthority)));
+#endif
+ }
+ }
+ }
+
+
+ private void SetUserDetails(K8SConfiguration k8SConfig, Context activeContext)
+ {
+ if (string.IsNullOrWhiteSpace(activeContext.ContextDetails.User))
+ {
+ return;
+ }
+
+ var userDetails = k8SConfig.Users.FirstOrDefault(c => c.Name.Equals(
+ activeContext.ContextDetails.User,
+ StringComparison.OrdinalIgnoreCase));
+
+ if (userDetails == null)
+ {
+ throw new KubeConfigException($"User not found for context {activeContext.Name} in kubeconfig");
+ }
+
+ if (userDetails.UserCredentials == null)
+ {
+ throw new KubeConfigException($"User credentials not found for user: {userDetails.Name} in kubeconfig");
+ }
+
+ var userCredentialsFound = false;
+
+ // Basic and bearer tokens are mutually exclusive
+ if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.Token))
+ {
+ AccessToken = userDetails.UserCredentials.Token;
+ userCredentialsFound = true;
+ }
+ else if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.UserName) &&
+ !string.IsNullOrWhiteSpace(userDetails.UserCredentials.Password))
+ {
+ Username = userDetails.UserCredentials.UserName;
+ Password = userDetails.UserCredentials.Password;
+ userCredentialsFound = true;
+ }
+
+ // Token and cert based auth can co-exist
+ if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.ClientCertificateData) &&
+ !string.IsNullOrWhiteSpace(userDetails.UserCredentials.ClientKeyData))
+ {
+ ClientCertificateData = userDetails.UserCredentials.ClientCertificateData;
+ ClientCertificateKeyData = userDetails.UserCredentials.ClientKeyData;
+ userCredentialsFound = true;
+ }
+
+ if (!string.IsNullOrWhiteSpace(userDetails.UserCredentials.ClientCertificate) &&
+ !string.IsNullOrWhiteSpace(userDetails.UserCredentials.ClientKey))
+ {
+ ClientCertificateFilePath = GetFullPath(k8SConfig, userDetails.UserCredentials.ClientCertificate);
+ ClientKeyFilePath = GetFullPath(k8SConfig, userDetails.UserCredentials.ClientKey);
+ userCredentialsFound = true;
+ }
+
+ if (userDetails.UserCredentials.AuthProvider != null)
+ {
+ if (userDetails.UserCredentials.AuthProvider.Config != null
+ && (userDetails.UserCredentials.AuthProvider.Config.ContainsKey("access-token")
+ || userDetails.UserCredentials.AuthProvider.Config.ContainsKey("id-token")))
+ {
+ switch (userDetails.UserCredentials.AuthProvider.Name)
+ {
+ case "azure":
+ throw new Exception("Please use the https://github.com/Azure/kubelogin credential plugin instead. See https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins for further details`");
+
+ case "gcp":
+ throw new Exception("Please use the \"gke-gcloud-auth-plugin\" credential plugin instead. See https://cloud.google.com/blog/products/containers-kubernetes/kubectl-auth-changes-in-gke for further details");
+ }
+ }
+ }
+
+ if (userDetails.UserCredentials.ExternalExecution != null)
+ {
+ if (string.IsNullOrWhiteSpace(userDetails.UserCredentials.ExternalExecution.Command))
+ {
+ throw new KubeConfigException(
+ "External command execution to receive user credentials must include a command to execute");
+ }
+
+ if (string.IsNullOrWhiteSpace(userDetails.UserCredentials.ExternalExecution.ApiVersion))
+ {
+ throw new KubeConfigException("External command execution missing ApiVersion key");
+ }
+
+ var response = ExecuteExternalCommand(userDetails.UserCredentials.ExternalExecution);
+ AccessToken = response.Status.Token;
+ // When reading ClientCertificateData from a config file it will be base64 encoded, and code later in the system (see CertUtils.GeneratePfx)
+ // expects ClientCertificateData and ClientCertificateKeyData to be base64 encoded because of this. However the string returned by external
+ // auth providers is the raw certificate and key PEM text, so we need to take that and base64 encoded it here so it can be decoded later.
+ ClientCertificateData = response.Status.ClientCertificateData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(response.Status.ClientCertificateData));
+ ClientCertificateKeyData = response.Status.ClientKeyData == null ? null : Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(response.Status.ClientKeyData));
+
+ userCredentialsFound = true;
+
+ // TODO: support client certificates here too.
+ if (AccessToken != null)
+ {
+ TokenProvider = new ExecTokenProvider(userDetails.UserCredentials.ExternalExecution);
+ }
+ }
+
+ if (!userCredentialsFound)
+ {
+ throw new KubeConfigException(
+ $"User: {userDetails.Name} does not have appropriate auth credentials in kubeconfig");
+ }
+ }
+
+ public static Process CreateRunnableExternalProcess(ExternalExecution config, EventHandler captureStdError = null)
+ {
+ if (config == null)
+ {
+ throw new ArgumentNullException(nameof(config));
+ }
+
+ var process = new Process();
+
+ process.StartInfo.EnvironmentVariables.Add("KUBERNETES_EXEC_INFO", $"{{ \"apiVersion\":\"{config.ApiVersion}\",\"kind\":\"ExecCredentials\",\"spec\":{{ \"interactive\":{Environment.UserInteractive.ToString().ToLower()} }} }}");
+ if (config.EnvironmentVariables != null)
+ {
+ foreach (var configEnvironmentVariable in config.EnvironmentVariables)
+ {
+ if (configEnvironmentVariable.ContainsKey("name") && configEnvironmentVariable.ContainsKey("value"))
+ {
+ var name = configEnvironmentVariable["name"];
+ process.StartInfo.EnvironmentVariables[name] = configEnvironmentVariable["value"];
+ }
+ else
+ {
+ var badVariable = string.Join(",", configEnvironmentVariable.Select(x => $"{x.Key}={x.Value}"));
+ throw new KubeConfigException($"Invalid environment variable defined: {badVariable}");
+ }
+ }
+ }
+
+ process.StartInfo.FileName = config.Command;
+ if (config.Arguments != null)
+ {
+ process.StartInfo.Arguments = string.Join(" ", config.Arguments);
+ }
+
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.RedirectStandardError = captureStdError != null;
+ process.StartInfo.UseShellExecute = false;
+ process.StartInfo.CreateNoWindow = true;
+
+ return process;
+ }
+
+ ///
+ /// Implementation of the proposal for out-of-tree client
+ /// authentication providers as described here --
+ /// https://github.com/kubernetes/community/blob/master/contributors/design-proposals/auth/kubectl-exec-plugins.md
+ /// Took inspiration from python exec_provider.py --
+ /// https://github.com/kubernetes-client/python-base/blob/master/config/exec_provider.py
+ ///
+ /// The external command execution configuration
+ ///
+ /// The token, client certificate data, and the client key data received from the external command execution
+ ///
+ public static ExecCredentialResponse ExecuteExternalCommand(ExternalExecution config)
+ {
+ if (config == null)
+ {
+ throw new ArgumentNullException(nameof(config));
+ }
+
+ var captureStdError = ExecStdError;
+ var process = CreateRunnableExternalProcess(config, captureStdError);
+
+ try
+ {
+ process.Start();
+ if (captureStdError != null)
+ {
+ process.ErrorDataReceived += captureStdError.Invoke;
+ process.BeginErrorReadLine();
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new KubeConfigException($"external exec failed due to: {ex.Message}");
+ }
+
+ try
+ {
+ if (!process.WaitForExit((int)(ExecTimeout.TotalMilliseconds)))
+ {
+ throw new KubeConfigException("external exec failed due to timeout");
+ }
+
+ var responseObject = JsonSerializer.Deserialize(
+ process.StandardOutput.ReadToEnd(),
+ ExecCredentialResponseContext.Default.ExecCredentialResponse);
+
+ if (responseObject == null || responseObject.ApiVersion != config.ApiVersion)
+ {
+ throw new KubeConfigException(
+ $"external exec failed because api version {responseObject.ApiVersion} does not match {config.ApiVersion}");
+ }
+
+ if (responseObject.Status.IsValid())
+ {
+ return responseObject;
+ }
+ else
+ {
+ throw new KubeConfigException($"external exec failed missing token or clientCertificateData field in plugin output");
+ }
+ }
+ catch (JsonException ex)
+ {
+ throw new KubeConfigException($"external exec failed due to failed deserialization process: {ex}");
+ }
+ catch (Exception ex)
+ {
+ throw new KubeConfigException($"external exec failed due to uncaught exception: {ex}");
+ }
+ }
+
+ ///
+ /// Loads entire Kube Config from default or explicit file path
+ ///
+ /// Explicit file path to kubeconfig. Set to null to use the default file path
+ /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
+ /// file is located. When , the paths will be considered to be relative to the current working directory.
+ /// Instance of the class
+ public static async Task LoadKubeConfigAsync(
+ string kubeconfigPath = null,
+ bool useRelativePaths = true)
+ {
+ var fileInfo = new FileInfo(kubeconfigPath ?? KubeConfigDefaultLocation);
+
+ return await LoadKubeConfigAsync(fileInfo, useRelativePaths).ConfigureAwait(false);
+ }
+
+ ///
+ /// Loads entire Kube Config from default or explicit file path
+ ///
+ /// Explicit file path to kubeconfig. Set to null to use the default file path
+ /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
+ /// file is located. When , the paths will be considered to be relative to the current working directory.
+ /// Instance of the class
+ public static K8SConfiguration LoadKubeConfig(string kubeconfigPath = null, bool useRelativePaths = true)
+ {
+ return LoadKubeConfigAsync(kubeconfigPath, useRelativePaths).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Loads Kube Config
+ ///
+ /// Kube config file contents
+ /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
+ /// file is located. When , the paths will be considered to be relative to the current working directory.
+ /// Instance of the class
+ public static async Task LoadKubeConfigAsync(
+ FileInfo kubeconfig,
+ bool useRelativePaths = true)
+ {
+ if (kubeconfig == null)
+ {
+ throw new ArgumentNullException(nameof(kubeconfig));
+ }
+
+
+ if (!kubeconfig.Exists)
+ {
+ throw new KubeConfigException($"kubeconfig file not found at {kubeconfig.FullName}");
+ }
+
+ using (var stream = kubeconfig.OpenRead())
+ {
+ var config = await KubernetesYaml.LoadFromStreamAsync(stream).ConfigureAwait(false);
+
+ if (useRelativePaths)
+ {
+ config.FileName = kubeconfig.FullName;
+ }
+
+ return config;
+ }
+ }
+
+ ///
+ /// Loads Kube Config
+ ///
+ /// Kube config file contents
+ /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
+ /// file is located. When , the paths will be considered to be relative to the current working directory.
+ /// Instance of the class
+ public static K8SConfiguration LoadKubeConfig(FileInfo kubeconfig, bool useRelativePaths = true)
+ {
+ return LoadKubeConfigAsync(kubeconfig, useRelativePaths).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Loads Kube Config
+ ///
+ /// Kube config file contents stream
+ /// Instance of the class
+ public static async Task LoadKubeConfigAsync(Stream kubeconfigStream)
+ {
+ return await KubernetesYaml.LoadFromStreamAsync(kubeconfigStream).ConfigureAwait(false);
+ }
+
+ ///
+ /// Loads Kube Config
+ ///
+ /// Kube config file contents stream
+ /// Instance of the class
+ public static K8SConfiguration LoadKubeConfig(Stream kubeconfigStream)
+ {
+ return LoadKubeConfigAsync(kubeconfigStream).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Loads Kube Config
+ ///
+ /// List of kube config file contents
+ /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
+ /// file is located. When , the paths will be considered to be relative to the current working directory.
+ /// Instance of the class
+ ///
+ /// The kube config files will be merges into a single , where first occurrence wins.
+ /// See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files.
+ ///
+ internal static K8SConfiguration LoadKubeConfig(FileInfo[] kubeConfigs, bool useRelativePaths = true)
+ {
+ return LoadKubeConfigAsync(kubeConfigs, useRelativePaths).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Loads Kube Config
+ ///
+ /// List of kube config file contents
+ /// When , the paths in the kubeconfig file will be considered to be relative to the directory in which the kubeconfig
+ /// file is located. When , the paths will be considered to be relative to the current working directory.
+ /// Instance of the class
+ ///
+ /// The kube config files will be merges into a single , where first occurrence wins.
+ /// See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files.
+ ///
+ internal static async Task LoadKubeConfigAsync(
+ FileInfo[] kubeConfigs,
+ bool useRelativePaths = true)
+ {
+ var basek8SConfig = await LoadKubeConfigAsync(kubeConfigs[0], useRelativePaths).ConfigureAwait(false);
+
+ for (var i = 1; i < kubeConfigs.Length; i++)
+ {
+ var mergek8SConfig = await LoadKubeConfigAsync(kubeConfigs[i], useRelativePaths).ConfigureAwait(false);
+ MergeKubeConfig(basek8SConfig, mergek8SConfig);
+ }
+
+ return basek8SConfig;
+ }
+
+ ///
+ /// Tries to get the full path to a file referenced from the Kubernetes configuration.
+ ///
+ ///
+ /// The Kubernetes configuration.
+ ///
+ ///
+ /// The path to resolve.
+ ///
+ ///
+ /// When possible a fully qualified path to the file.
+ ///
+ ///
+ /// For example, if the configuration file is at "C:\Users\me\kube.config" and path is "ca.crt",
+ /// this will return "C:\Users\me\ca.crt". Similarly, if path is "D:\ca.cart", this will return
+ /// "D:\ca.crt".
+ ///
+ private static string GetFullPath(K8SConfiguration configuration, string path)
+ {
+ // If we don't have a file name,
+ if (string.IsNullOrWhiteSpace(configuration.FileName) || Path.IsPathRooted(path))
+ {
+ return path;
+ }
+ else
+ {
+ return Path.Combine(Path.GetDirectoryName(configuration.FileName), path);
+ }
+ }
+
+ ///
+ /// Merges kube config files together, preferring configuration present in the base config over the merge config.
+ ///
+ /// The to merge into
+ /// The to merge from
+ private static void MergeKubeConfig(K8SConfiguration basek8SConfig, K8SConfiguration mergek8SConfig)
+ {
+ // For scalar values, prefer local values
+ basek8SConfig.CurrentContext = basek8SConfig.CurrentContext ?? mergek8SConfig.CurrentContext;
+ basek8SConfig.FileName = basek8SConfig.FileName ?? mergek8SConfig.FileName;
+
+ // Kinds must match in kube config, otherwise throw.
+ if (basek8SConfig.Kind != mergek8SConfig.Kind)
+ {
+ throw new KubeConfigException(
+ $"kubeconfig \"kind\" are different between {basek8SConfig.FileName} and {mergek8SConfig.FileName}");
+ }
+
+ // Note, Clusters, Contexts, and Extensions are map-like in config despite being represented as a list here:
+ // https://github.com/kubernetes/client-go/blob/ede92e0fe62deed512d9ceb8bf4186db9f3776ff/tools/clientcmd/api/types.go#L238
+ // basek8SConfig.Extensions = MergeLists(basek8SConfig.Extensions, mergek8SConfig.Extensions, (s) => s.Name).ToList();
+ basek8SConfig.Clusters = MergeLists(basek8SConfig.Clusters, mergek8SConfig.Clusters, (s) => s.Name).ToList();
+ basek8SConfig.Users = MergeLists(basek8SConfig.Users, mergek8SConfig.Users, (s) => s.Name).ToList();
+ basek8SConfig.Contexts = MergeLists(basek8SConfig.Contexts, mergek8SConfig.Contexts, (s) => s.Name).ToList();
+ }
+
+ private static IEnumerable MergeLists(IEnumerable baseList, IEnumerable mergeList,
+ Func getNameFunc)
+ {
+ if (mergeList != null && mergeList.Any())
+ {
+ var mapping = new Dictionary();
+ foreach (var item in baseList)
+ {
+ mapping[getNameFunc(item)] = item;
+ }
+
+ foreach (var item in mergeList)
+ {
+ var name = getNameFunc(item);
+ if (!mapping.ContainsKey(name))
+ {
+ mapping[name] = item;
+ }
+ }
+
+ return mapping.Values;
+ }
+
+ return baseList;
+ }
+ }
+}
diff --git a/src/KubernetesClient.Aot/KubernetesYaml.cs b/src/KubernetesClient.Aot/KubernetesYaml.cs
new file mode 100644
index 000000000..5530a2e02
--- /dev/null
+++ b/src/KubernetesClient.Aot/KubernetesYaml.cs
@@ -0,0 +1,171 @@
+using System.Text;
+using YamlDotNet.Core;
+using YamlDotNet.Core.Events;
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.NamingConventions;
+
+namespace k8s
+{
+ ///
+ /// This is a utility class that helps you load objects from YAML files.
+ ///
+ internal static class KubernetesYaml
+ {
+ private static StaticDeserializerBuilder CommonDeserializerBuilder =>
+ new StaticDeserializerBuilder(new k8s.KubeConfigModels.StaticContext())
+ .WithNamingConvention(CamelCaseNamingConvention.Instance)
+ .WithTypeConverter(new IntOrStringYamlConverter())
+ .WithTypeConverter(new ByteArrayStringYamlConverter())
+ .WithTypeConverter(new ResourceQuantityYamlConverter())
+ .WithTypeConverter(new KubernetesDateTimeYamlConverter())
+ .WithTypeConverter(new KubernetesDateTimeOffsetYamlConverter())
+ .WithAttemptingUnquotedStringTypeDeserialization()
+ ;
+
+ private static readonly IDeserializer Deserializer =
+ CommonDeserializerBuilder
+ .IgnoreUnmatchedProperties()
+ .Build();
+ private static IDeserializer GetDeserializer(bool strict) => Deserializer;
+
+ private static readonly IValueSerializer Serializer =
+ new StaticSerializerBuilder(new k8s.KubeConfigModels.StaticContext())
+ .DisableAliases()
+ .WithNamingConvention(CamelCaseNamingConvention.Instance)
+ .WithTypeConverter(new IntOrStringYamlConverter())
+ .WithTypeConverter(new ByteArrayStringYamlConverter())
+ .WithTypeConverter(new ResourceQuantityYamlConverter())
+ .WithTypeConverter(new KubernetesDateTimeYamlConverter())
+ .WithTypeConverter(new KubernetesDateTimeOffsetYamlConverter())
+ .WithEventEmitter(e => new StringQuotingEmitter(e))
+ .WithEventEmitter(e => new FloatEmitter(e))
+ .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull)
+ .BuildValueSerializer();
+
+ private class ByteArrayStringYamlConverter : IYamlTypeConverter
+ {
+ public bool Accepts(Type type)
+ {
+ return type == typeof(byte[]);
+ }
+
+ public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
+ {
+ if (parser?.Current is Scalar scalar)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(scalar.Value))
+ {
+ return null;
+ }
+
+ return Convert.FromBase64String(scalar.Value);
+ }
+ finally
+ {
+ parser.MoveNext();
+ }
+ }
+
+ throw new InvalidOperationException(parser.Current?.ToString());
+ }
+
+ public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer)
+ {
+ if (value == null)
+ {
+ emitter.Emit(new Scalar(string.Empty));
+ return;
+ }
+
+ var obj = (byte[])value;
+ var encoded = Convert.ToBase64String(obj);
+ emitter.Emit(new Scalar(encoded));
+ }
+ }
+
+ public static async Task LoadFromStreamAsync(Stream stream, bool strict = false)
+ {
+ var reader = new StreamReader(stream);
+ var content = await reader.ReadToEndAsync().ConfigureAwait(false);
+ return Deserialize(content, strict);
+ }
+
+ public static async Task LoadFromFileAsync(string file, bool strict = false)
+ {
+ using (var fs = File.OpenRead(file))
+ {
+ return await LoadFromStreamAsync(fs, strict).ConfigureAwait(false);
+ }
+ }
+
+ [Obsolete("use Deserialize")]
+ public static T LoadFromString(string content, bool strict = false)
+ {
+ return Deserialize(content, strict);
+ }
+
+ [Obsolete("use Serialize")]
+ public static string SaveToString(T value)
+ {
+ return Serialize(value);
+ }
+
+ public static TValue Deserialize(string yaml, bool strict = false)
+ {
+ using var reader = new StringReader(yaml);
+ return GetDeserializer(strict).Deserialize(new MergingParser(new Parser(reader)));
+ }
+
+ public static TValue Deserialize(Stream yaml, bool strict = false)
+ {
+ using var reader = new StreamReader(yaml);
+ return GetDeserializer(strict).Deserialize(new MergingParser(new Parser(reader)));
+ }
+
+ public static string SerializeAll(IEnumerable