Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add ACI Spot virtual nodes support to virtual kubelet #192

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
63da061
Add priority as annotation to use spot containers for virtual kubelet
telotlik Apr 5, 2022
151530d
Add new test to validate null priority
telotlik Apr 6, 2022
d8fda88
Updating priority check in pod annotation
telotlik Apr 6, 2022
74fd22c
Update tests and add priority annotation const
telotlik Apr 7, 2022
3f23906
Update priority string check
telotlik Apr 7, 2022
d803498
Iterate on ACI Spot CG on Virtual Kubelets - new function for propert…
suselva Apr 13, 2022
b3bc432
Merge pull request #1 from telotlik/users/suselva/aci-spot
suselva Apr 21, 2022
cc15b33
api version when using priority
fnuarnav Apr 21, 2022
c0e15d8
update: set apiVersion based on extensible list of Tags
fnuarnav May 2, 2022
fb1a943
use VersioProvider to check ContainerGroupProperties
fnuarnav May 3, 2022
bbc2355
fix indentation; validate version format
fnuarnav May 4, 2022
c4c3b0a
Revert "fix indentation; validate version format"
fnuarnav May 4, 2022
bc57388
version set in code follows correct format
fnuarnav May 4, 2022
49c3e26
added unit tests for versioning
fnuarnav May 4, 2022
2843053
update logging; use more c# style documentation comments
fnuarnav May 5, 2022
f2a006a
remove print stmt; log uri in create
fnuarnav May 6, 2022
7f42078
fix indents
fnuarnav May 6, 2022
2b1c322
use virtual-kubelet.io/priority; update comment
fnuarnav May 6, 2022
a35c90a
use tag name virtual-kubelet.io-proprity
fnuarnav May 6, 2022
b75b772
fix typo
fnuarnav May 6, 2022
bd6bc81
set priority Tag with correct values only when priority annotaiton is…
fnuarnav May 6, 2022
e222422
clean up code
fnuarnav May 6, 2022
d780b79
define priorityTagNae as a constant
fnuarnav May 9, 2022
35334f6
Merge pull request #3 from telotlik/users/arnav/review-comments
fnuarnav May 9, 2022
7c147e3
Merge remote-tracking branch 'upstream/master' into telotlik/VKACISpo…
fnuarnav May 9, 2022
79ce469
Merge branch 'telotlik/VKACISpotContainers' into users/arnav/api-version
fnuarnav May 9, 2022
9613c46
fixed typo in comment
fnuarnav May 9, 2022
122d8bc
try adding parallelism to avoid timeout
fnuarnav May 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
version: 2
jobs:
validate:
parallelism: 2
docker:
- image: golang:1.16
working_directory: /go/src/github.com/virtual-kubelet/azure-aci
Expand Down
9 changes: 8 additions & 1 deletion client/aci/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,26 @@ import (
"net/url"

"github.com/virtual-kubelet/azure-aci/client/api"
"github.com/virtual-kubelet/virtual-kubelet/log"
)

// CreateContainerGroup creates a new Azure Container Instance with the
// provided properties.
// From: https://docs.microsoft.com/en-us/rest/api/container-instances/containergroups/createorupdate
func (c *Client) CreateContainerGroup(ctx context.Context, resourceGroup, containerGroupName string, containerGroup ContainerGroup) (*ContainerGroup, error) {

// create a new VersionProvider object
versionProvider := newVersionProvider(apiVersion)

urlParams := url.Values{
"api-version": []string{apiVersion},
// use versionProvider to get the correct min api version
"api-version": []string{versionProvider.getVersion(containerGroup, ctx).finalVersion},
}

// Create the url.
uri := api.ResolveRelative(c.auth.ResourceManagerEndpoint, containerGroupURLPath)
uri += "?" + url.Values(urlParams).Encode()
log.G(ctx).Infof("Using url '%s' for Create", uri)

// Create the body for the request.
b := new(bytes.Buffer)
Expand Down
19 changes: 15 additions & 4 deletions client/aci/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ const (
OnFailure ContainerGroupRestartPolicy = "OnFailure"
)

// ContainerGroupPriority enumerates the values for container group priority.
type ContainerGroupPriority string

const (
// Regular specifies the regular priority for container group restart priority.
Regular ContainerGroupPriority = "Regular"
// Spot specifies the spot priority for container group restart priority.
Spot ContainerGroupPriority = "Spot"
)

// ContainerNetworkProtocol enumerates the values for container network protocol.
type ContainerNetworkProtocol string

Expand Down Expand Up @@ -94,9 +104,10 @@ type ContainerGroupProperties struct {
Volumes []Volume `json:"volumes,omitempty"`
InstanceView ContainerGroupPropertiesInstanceView `json:"instanceView,omitempty"`
Diagnostics *ContainerGroupDiagnostics `json:"diagnostics,omitempty"`
SubnetIds []*SubnetIdDefinition `json:"subnetIds,omitempty"`
SubnetIds []*SubnetIdDefinition `json:"subnetIds,omitempty"`
Extensions []*Extension `json:"extensions,omitempty"`
DNSConfig *DNSConfig `json:"dnsConfig,omitempty"`
Priority ContainerGroupPriority `json:"priority,omitempty"`
}

// ContainerGroupPropertiesInstanceView is the instance view of the container group. Only valid in response.
Expand All @@ -105,7 +116,7 @@ type ContainerGroupPropertiesInstanceView struct {
State string `json:"state,omitempty"`
}

// SubnetIdDefinition is the subnet ID, the format should be
// SubnetIdDefinition is the subnet ID, the format should be
// /subscriptions/{subscriptionID}/resourceGroups/{ResourceGroup}/providers/Microsoft.Network/virtualNetworks/{VNET}/subnets/{Subnet}
type SubnetIdDefinition struct {
ID string `json:"id,omitempty"`
Expand Down Expand Up @@ -242,7 +253,7 @@ type GPUSKU string

const (
// K80 specifies the K80 GPU SKU
K80 GPUSKU = "K80"
K80 GPUSKU = "K80"
// P100 specifies the P100 GPU SKU
P100 GPUSKU = "P100"
// V100 specifies the V100 GPU SKU
Expand Down Expand Up @@ -460,7 +471,7 @@ type ExtensionType string

// Supported extension types
const (
ExtensionTypeKubeProxy ExtensionType = "kube-proxy"
ExtensionTypeKubeProxy ExtensionType = "kube-proxy"
ExtensionTypeRealtimeMetrics ExtensionType = "realtime-metrics"
)

Expand Down
70 changes: 70 additions & 0 deletions client/aci/versioning.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package aci

import (
"github.com/virtual-kubelet/virtual-kubelet/log"
"context"
)

//<summary>
// map of minimum api version required for different properties
// TODO: read from a separate json file if it gets too large
//</summary>
var minVersionSupport = map[string]string {
"Priority": "2021-10-01",
}

//<summary>
// VersionProvider struct for selecting api version based on various properties
// Maintains the api version selected after checking certain property
//<summary>
type VersionProvider struct {
finalVersion string
}

//<summary>
// creates a new instance of VErsionProvider, and sets default version
// assumes api version to be of the format YYYY-mm-dd[-suffix]
// assumes that the api version format will not be violated
//</summary>
//<param name="defaultVersion"> The default api version </param>
//<returns>
// reference to an instance of the verison provider object
//</returns>
func newVersionProvider(defaultVersion string) (*VersionProvider) {
return &VersionProvider{defaultVersion}
}

//<summary>
// get the api version for the specific ContainerGroup instance based on various properties
//</summary>
//<param name="containerGroup"> the ContainerGroup instance for which version is to be selected </param>
//<param name="ctx"> the Context to be used for logging </param>
//<returns>
// reference to an instance of VersionProvider with the finalVersion field updated
//</returns>
func (versionProvider *VersionProvider) getVersion(containerGroup ContainerGroup, ctx context.Context) (* VersionProvider) {

versionProvider.setVersionFromProperty(string(containerGroup.ContainerGroupProperties.Priority), "Priority", ctx)

log.G(ctx).Infof("API Version set to %s \n", versionProvider.finalVersion)
return versionProvider
}

//<summary>
// find the min api version for a string property based on the value in minVersionSupport map
// assumes that the api version always uses the correct format YYYY-mm-dd[-suffix]
//</summary>
//<param name="property">the string value of some property field that should be checked<param>
//<param name="keyRef">the key for the property in the minVersionSupport map</param>
//<param name="ctx">the context to be used for logging</param>
//<returns>
// reference to an instance of VersionProvider with the finalVersion field updated
//</returns>
func (versionProvider *VersionProvider) setVersionFromProperty(property string, keyRef string, ctx context.Context) (*VersionProvider) {
minVersion, ok := minVersionSupport[keyRef]
if len(property) > 0 && ok && versionProvider.finalVersion < minVersion {
versionProvider.finalVersion = minVersion
}
log.G(ctx).Infof("Selected API Version %s for property %s with value %s \n", versionProvider.finalVersion, keyRef, property)
return versionProvider
}
85 changes: 85 additions & 0 deletions client/aci/versioning_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package aci

import (
"testing"
"context"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)

// tests setlecting version when property is present in map
func TestSetVersionFromPropertyInMap(t *testing.T) {
key := "Priority"
defaultVersion := apiVersion
versionProvider:= newVersionProvider(defaultVersion)

// should use defualt version when property is not populated
versionProvider.setVersionFromProperty("", key, context.Background())
assert.Check(t, is.Equal(versionProvider.finalVersion, defaultVersion), "Default version should be used when a property value is empty")

// should use api version present in map if priority is Spot
versionProvider.setVersionFromProperty("Spot", key, context.Background())
assert.Check(t, is.Equal(versionProvider.finalVersion, minVersionSupport[key]), "When a version is available for the property in map, the final version should be >= min api version for the property")

// should use api version present in map if priority is Regular
versionProvider.setVersionFromProperty("Regular", key, context.Background())
assert.Check(t, is.Equal(versionProvider.finalVersion, minVersionSupport[key]), "When a version is available for the property in map, the final version should be >= min api version for the property")

}

// tests selecting version when a property is present in map with version < defualtVersion
func TestSetLowerVersionFromPropertyInMap (t *testing.T) {
key := "Priority"
largeDefaultVersion := "9999-99-99"
versionProvider := newVersionProvider(largeDefaultVersion)

// should use largeDefaultVersion as it is > the min api version for this key
versionProvider.setVersionFromProperty("Regular", key, context.Background())
assert.Check(t, versionProvider.finalVersion >= minVersionSupport[key], "Use larger version among default and min api versions for various properties")
}

// test selecting version when property is not present in map
func TestSetVersionFromPropertyNotInMap(t *testing.T) {
key := "someUnknownKey"
defaultVersion := apiVersion
versionProvider:= newVersionProvider(defaultVersion)

// should use default version when property is not present in map
versionProvider.setVersionFromProperty("propertyValue", key, context.Background())
assert.Check(t, versionProvider.finalVersion == defaultVersion, "Default version should be used when no version for a property is available in map")
}

// test getVersion for ContainerGroup with Priority
func TestGetVersionForContainerGroupWithPriority(t *testing.T) {
key := "Priority"
versionProvider := newVersionProvider(apiVersion)
containerGroup := ContainerGroup{
Location: "eastus",
ContainerGroupProperties: ContainerGroupProperties{
Priority: Spot,
OsType: Linux,
},
}

// should use api version in map when priority is set for a containerGroup
versionProvider.getVersion(containerGroup, context.Background())
assert.Check(t, is.Equal(versionProvider.finalVersion, minVersionSupport[key]), "Use api version in map when priority is set for a containerGroup")

}

// test getVersion for containerGroup without Priority
func TestGetVersionForContainerGroupWithoutPriority(t *testing.T) {
defaultVersion := apiVersion
versionProvider := newVersionProvider(defaultVersion)
containerGroup := ContainerGroup{
Location: "eastus",
ContainerGroupProperties: ContainerGroupProperties{
OsType: Linux,
},
}

// should use default api version when priority is not set for a containerGroup
versionProvider.getVersion(containerGroup, context.Background())
assert.Check(t, is.Equal(versionProvider.finalVersion, defaultVersion), "Use default api version when priority is not set for a containerGroup")

}
34 changes: 34 additions & 0 deletions provider/aci.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ const (
gpuTypeAnnotation = "virtual-kubelet.io/gpu-type"
)

const (
priorityTypeAnnotation = "virtual-kubelet.io/priority"
priorityTagName = "virtual-kubelet.io-priority"
)

const (
statusReasonPodDeleted = "NotFound"
statusMessagePodDeleted = "The pod may have been deleted from the provider"
Expand Down Expand Up @@ -724,6 +729,11 @@ func (p *ACIProvider) CreatePod(ctx context.Context, pod *v1.Pod) error {
"CreationTimestamp": podCreationTimestamp,
}

//set container group priority property and Tag
if err := setContainerGroupPriority(&containerGroup, pod); err != nil {
return fmt.Errorf("error setting container group priority: %v", err)
}

p.amendVnetResources(&containerGroup, pod)
if p.realtimeMetricsExtension != nil {
containerGroup.ContainerGroupProperties.Extensions = append(containerGroup.ContainerGroupProperties.Extensions, p.realtimeMetricsExtension)
Expand All @@ -734,6 +744,30 @@ func (p *ACIProvider) CreatePod(ctx context.Context, pod *v1.Pod) error {
return p.createContainerGroup(ctx, pod.Namespace, pod.Name, &containerGroup)
}

// Set the Container Group Priority Property and Tag
// Set the Container Group Priority Tag with Tag name virtual-kubelet.io-priority
// value is set based on the priorityTypeAnnotation field under annotations in the pod spec
// Accepted Values : Regular, Spot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the priority is not an explicit parameter to this function, this comment should explain how priority is passed in, if we already mention what the accepted values are :)

Additionally, as far as I can tell from the code, it is also allowed not to specify it at all, so I'd recommend mentioning that too.

func setContainerGroupPriority(containerGroup *aci.ContainerGroup, pod *v1.Pod) error {

if pod.Annotations != nil {
priority, priorityExists := pod.Annotations[priorityTypeAnnotation]
if priorityExists {
if strings.EqualFold(priority, string(aci.Spot)) {
containerGroup.ContainerGroupProperties.Priority = aci.Spot
containerGroup.Tags[priorityTagName] = string(aci.Spot)
} else if strings.EqualFold(priority, string(aci.Regular)) {
containerGroup.ContainerGroupProperties.Priority = aci.Regular
containerGroup.Tags[priorityTagName] = string(aci.Regular)
} else {
return fmt.Errorf("The pod requires either Regular or Spot priority. Invalid value %s", priority)
}
}
}

return nil
}

func (p *ACIProvider) createContainerGroup(ctx context.Context, podNS, podName string, cg *aci.ContainerGroup) error {
ctx, span := trace.StartSpan(ctx, "aci.createContainerGroup")
defer span.End()
Expand Down
Loading