Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
marcoandredinis committed Sep 30, 2024
1 parent 95e3d60 commit 6b3b747
Show file tree
Hide file tree
Showing 8 changed files with 706 additions and 36 deletions.
63 changes: 63 additions & 0 deletions api/types/autodiscover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
Copyright 2024 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package types

// List of Auto Discover EC2 issues identifiers.
// This value is used to populate the UserTasks.Spec.IssueType for Discover EC2 tasks.
// The Web UI will then use those identifiers to show detailed instructions on how to fix the issue.
const (
// AutoDiscoverEC2IssueEICEFailedToCreateNode is used when the EICE flow fails to create a node.
// This can happen when the Node does not have a valid PrivateIPAddress.
// This is very unlekly and should only happen if the AWS API returns an unexpected response.
AutoDiscoverEC2IssueEICEFailedToCreateNode = "ec2-eice-create-node"

// AutoDiscoverEC2IssueEICEFailedToUpsertNode is used when the EICE flow fails to upsert a node into the cluster.
// This is very unlekly and should only happen
// - if the Discovery system role was changed
// - if the Node resource validation was changed on the Auth and not on the DiscoveryService
// - or because of a network error
AutoDiscoverEC2IssueEICEFailedToUpsertNode = "ec2-eice-upsert-node"

// AutoDiscoverEC2IssueScriptInstanceNotRegistered is used to identify instances that failed to auto-enroll
// because they are not present in Amazon Systems Manager.
// This usually means that the Instance does not have the SSM Agent running,
// or that the instance's IAM Profile does not allow have the managed IAM Policy AmazonSSMManagedInstanceCore assigned to it.
AutoDiscoverEC2IssueScriptInstanceNotRegistered = "ec2-ssm-agent-not-registered"

// AutoDiscoverEC2IssueScriptInstanceConnectionLost is used to identify instances that failed to auto-enroll
// because the agent lost connection to the in Amazon Systems Manager.
// This can happen if the user changed some setting in the instance's network or IAM profile.
AutoDiscoverEC2IssueScriptInstanceConnectionLost = "ec2-ssm-agent-connection-lost"

// AutoDiscoverEC2IssueScriptInstanceUnsupportedOS is used to identify instances that failed to auto-enroll
// because its OS is not supported by teleport.
// This can happen if the instance is running Windows.
AutoDiscoverEC2IssueScriptInstanceUnsupportedOS = "ec2-ssm-unsupported-os"

// AutoDiscoverEC2IssueScriptFailure is used to identify instances that failed to auto-enroll
// because the installation script failed.
// This can happen for multiple reasons.
// In this case, the invocation url must be included in the report, so that users can see what was wrong.
AutoDiscoverEC2IssueScriptFailure = "ec2-ssm-script-failure"

// AutoDiscoverEC2IssueInvocationFailure is used to identify instances that failed to auto-enroll
// because the invocation failed.
// This happens when there's a failure with
// This can happen for multiple reasons.
// In this case, the invocation url must be included in the report, so that users can see what
AutoDiscoverEC2IssueInvocationFailure = "ec2-ssm-script-failure"
)
139 changes: 119 additions & 20 deletions api/types/usertasks/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,77 +19,176 @@
package usertasks

import (
"encoding/binary"
"slices"
"time"

"github.com/google/uuid"
"github.com/gravitational/trace"
"google.golang.org/protobuf/types/known/timestamppb"

headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
usertasksv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/usertasks/v1"
"github.com/gravitational/teleport/api/types"
)

// UserTaskOption defines a function that mutates a User Task.
type UserTaskOption func(ut *usertasksv1.UserTask)

// WithExpiration sets the expiration of the UserTask resource.
func WithExpiration(t time.Time) func(ut *usertasksv1.UserTask) {
return func(ut *usertasksv1.UserTask) {
ut.Metadata.Expires = timestamppb.New(t)
}
}

// NewUserTask creates a new UserTask object.
// It validates the object before returning it.
func NewUserTask(name string, spec *usertasksv1.UserTaskSpec) (*usertasksv1.UserTask, error) {
cj := &usertasksv1.UserTask{
func NewUserTask(name string, spec *usertasksv1.UserTaskSpec, opts ...UserTaskOption) (*usertasksv1.UserTask, error) {
ut := &usertasksv1.UserTask{
Kind: types.KindUserTask,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: name,
},
Spec: spec,
}
for _, o := range opts {
o(ut)
}

if err := ValidateUserTask(cj); err != nil {
if err := ValidateUserTask(ut); err != nil {
return nil, trace.Wrap(err)
}

return cj, nil
return ut, nil
}

// NewDiscoverEC2UserTask creates a new User Task of DiscoverEC2 type.
func NewDiscoverEC2UserTask(spec *usertasksv1.UserTaskSpec, opts ...UserTaskOption) (*usertasksv1.UserTask, error) {
taskName := TaskNameForDiscoverEC2(spec.Integration, spec.IssueType)
return NewUserTask(taskName, spec, opts...)
}

const (
// TaskStateOpen identifies an issue with an instance that is not yet resolved.
TaskStateOpen = "OPEN"
// TaskStateResolved identifies an issue with an instance that is resolved.
TaskStateResolved = "RESOLVED"
)

var validTaskStates = []string{TaskStateOpen, TaskStateResolved}

const (
// TaskTypeDiscoverEC2 identifies a User Tasks that is created
// when an auto-enrollment of an EC2 instance fails.
// UserTasks that have this Task Type must include the DiscoverEC2 field.
TaskTypeDiscoverEC2 = "discover-ec2"
)

// discoverEC2IssueTypes is a list of issue types that can occur when trying to auto enroll EC2 instances.
var discoverEC2IssueTypes = []string{
types.AutoDiscoverEC2IssueEICEFailedToCreateNode,
types.AutoDiscoverEC2IssueScriptInstanceNotRegistered,
types.AutoDiscoverEC2IssueScriptInstanceConnectionLost,
types.AutoDiscoverEC2IssueScriptInstanceUnsupportedOS,
types.AutoDiscoverEC2IssueScriptFailure,
}

// ValidateUserTask validates the UserTask object without modifying it.
func ValidateUserTask(uit *usertasksv1.UserTask) error {
func ValidateUserTask(ut *usertasksv1.UserTask) error {
switch {
case uit.GetKind() != types.KindUserTask:
case ut.GetKind() != types.KindUserTask:
return trace.BadParameter("invalid kind")
case uit.GetVersion() != types.V1:
case ut.GetVersion() != types.V1:
return trace.BadParameter("invalid version")
case uit.GetSubKind() != "":
case ut.GetSubKind() != "":
return trace.BadParameter("invalid sub kind, must be empty")
case uit.GetMetadata() == nil:
case ut.GetMetadata() == nil:
return trace.BadParameter("user task metadata is nil")
case uit.Metadata.GetName() == "":
case ut.Metadata.GetName() == "":
return trace.BadParameter("user task name is empty")
case uit.GetSpec() == nil:
case ut.GetSpec() == nil:
return trace.BadParameter("user task spec is nil")
case uit.GetSpec().Integration == "":
return trace.BadParameter("integration is required")
case !slices.Contains(validTaskStates, ut.GetSpec().State):
return trace.BadParameter("invalid task state, allowed values: %v", validTaskStates)
}

switch uit.Spec.TaskType {
switch ut.Spec.TaskType {
case TaskTypeDiscoverEC2:
if err := validateDiscoverEC2TaskType(uit); err != nil {
if err := validateDiscoverEC2TaskType(ut); err != nil {
return trace.Wrap(err)
}
default:
return trace.BadParameter("task type %q is not valid", uit.Spec.TaskType)
return trace.BadParameter("task type %q is not valid", ut.Spec.TaskType)
}

return nil
}

func validateDiscoverEC2TaskType(uit *usertasksv1.UserTask) error {
if uit.Spec.DiscoverEc2 == nil {
func validateDiscoverEC2TaskType(ut *usertasksv1.UserTask) error {
if ut.GetSpec().Integration == "" {
return trace.BadParameter("integration is required")
}

expectedTaskName := TaskNameForDiscoverEC2(ut.Spec.Integration, ut.Spec.IssueType)
if ut.Metadata.GetName() != expectedTaskName {
return trace.BadParameter("task name is invalid for integration %q and issue type %q, expected %q, got %q",
ut.Spec.Integration,
ut.Spec.TaskType,
expectedTaskName,
ut.Metadata.GetName(),
)
}

if ut.GetSpec().DiscoverEc2 == nil {
return trace.BadParameter("%s requires the discover_ec2 field", TaskTypeDiscoverEC2)
}
if uit.Spec.IssueType == "" {
return trace.BadParameter("issue type is required")

if !slices.Contains(discoverEC2IssueTypes, ut.GetSpec().IssueType) {
return trace.BadParameter("invalid issue type state, allowed values: %v", discoverEC2IssueTypes)
}

if len(ut.Spec.DiscoverEc2.Instances) == 0 {
return trace.BadParameter("at least one instance is required")
}
for instanceID, instanceIssue := range ut.Spec.DiscoverEc2.Instances {
if instanceID == "" {
return trace.BadParameter("instance id in discover_ec2.instances map is required")
}
if instanceIssue.InstanceId == "" {
return trace.BadParameter("instance id in discover_ec2.instances field is required")
}
if instanceID != instanceIssue.InstanceId {
return trace.BadParameter("instance id in discover_ec2.instances map and field are different")
}
if instanceIssue.AccountId == "" {
return trace.BadParameter("account id in discover_ec2.instances field is required")
}
if instanceIssue.Region == "" {
return trace.BadParameter("region in discover_ec2.instances field is required")
}
if instanceIssue.DiscoveryConfig == "" {
return trace.BadParameter("discovery config in discover_ec2.instances field is required")
}
if instanceIssue.DiscoveryGroup == "" {
return trace.BadParameter("discovery group in discover_ec2.instances field is required")
}
}

return nil
}

// TaskNameForDiscoverEC2 returns a deterministic name for the DiscoverEC2 task type.
// This method is used to ensure a single UserTask is created to report issues in enrolling EC2 instances for a given integration and issue type.
func TaskNameForDiscoverEC2(integration string, issueType string) string {
var bs []byte
bs = append(bs, binary.LittleEndian.AppendUint64(nil, uint64(len(integration)))...)
bs = append(bs, []byte(integration)...)
bs = append(bs, binary.LittleEndian.AppendUint64(nil, uint64(len(issueType)))...)
bs = append(bs, []byte(issueType)...)
return uuid.NewSHA1(discoverEC2Namespace, bs).String()
}

// discoverEC2Namespace is an UUID that represents the name space to be used for generating UUIDs for DiscoverEC2 User Task names.
var discoverEC2Namespace = uuid.Must(uuid.Parse("6ba7b815-9dad-11d1-80b4-00c04fd430c8"))
Loading

0 comments on commit 6b3b747

Please sign in to comment.