Skip to content

Commit bee670c

Browse files
tfo unlock
1 parent 402a109 commit bee670c

File tree

5 files changed

+163
-40
lines changed

5 files changed

+163
-40
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,4 @@ The server will be accessible at `http://localhost:3000` (or the specified port)
5151

5252
## Testing
5353

54-
Use tools like `curl` or Postman to test the API endpoints. Verify that the responses match your expectations.
54+
Use tools like `curl` or Postman to test the API endpoints. Verify that the responses match your expectations.

go.mod

+2-5
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module github.com/galleybytes/terraform-operator-api
33
go 1.19
44

55
require (
6+
github.com/akyoto/cache v1.0.6
67
github.com/galleybytes/terraform-operator v0.14.0
7-
github.com/gammazero/deque v0.2.1
88
github.com/gin-gonic/gin v1.8.1
99
github.com/golang-jwt/jwt/v4 v4.4.3
1010
github.com/isaaguilar/kedge v0.0.0-20230623005919-25931c711d84
@@ -19,7 +19,6 @@ require (
1919
require (
2020
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
2121
github.com/MakeNowJust/heredoc v1.0.0 // indirect
22-
github.com/akyoto/cache v1.0.6 // indirect
2322
github.com/beevik/etree v1.1.0 // indirect
2423
github.com/chai2010/gettext-go v1.0.2 // indirect
2524
github.com/crewjam/httperr v0.2.0 // indirect
@@ -40,7 +39,6 @@ require (
4039
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
4140
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
4241
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
43-
github.com/pmezard/go-difflib v1.0.0 // indirect
4442
github.com/russellhaering/goxmldsig v1.2.0 // indirect
4543
github.com/russross/blackfriday/v2 v2.1.0 // indirect
4644
github.com/spf13/cobra v1.6.0 // indirect
@@ -96,7 +94,6 @@ require (
9694
)
9795

9896
require (
99-
github.com/DATA-DOG/go-sqlmock v1.5.0
10097
github.com/creack/pty v1.1.18
10198
github.com/crewjam/saml v0.4.13
10299
github.com/fsnotify/fsnotify v1.6.0 // indirect
@@ -131,7 +128,7 @@ require (
131128
github.com/spf13/cast v1.5.0 // indirect
132129
github.com/spf13/jwalterweatherman v1.1.0 // indirect
133130
github.com/spf13/pflag v1.0.5
134-
github.com/stretchr/testify v1.8.4
131+
github.com/stretchr/testify v1.8.4 // indirect
135132
github.com/subosito/gotenv v1.4.1 // indirect
136133
github.com/ucarion/saml v0.1.2
137134
github.com/ugorji/go/codec v1.2.7 // indirect

go.sum

-9
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl
4040
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
4141
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
4242
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
43-
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
44-
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
4543
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
4644
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
4745
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@@ -115,12 +113,8 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
115113
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
116114
github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE=
117115
github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
118-
github.com/galleybytes/terraform-operator v0.12.1 h1:sqLZtnxlNgIl0psnBjnjwQ676P+q2NIhcoUfuGcHyYY=
119-
github.com/galleybytes/terraform-operator v0.12.1/go.mod h1:UBmC5dPK2dBA09AjLNW4szw5VqQ8mndXrR8Gp97GRtE=
120116
github.com/galleybytes/terraform-operator v0.14.0 h1:fmknK+CyaG12Oz0wE0Oq72zpnRkzx0zMwcLCF/am0Vs=
121117
github.com/galleybytes/terraform-operator v0.14.0/go.mod h1:UBmC5dPK2dBA09AjLNW4szw5VqQ8mndXrR8Gp97GRtE=
122-
github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0=
123-
github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU=
124118
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
125119
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
126120
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -455,7 +449,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
455449
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
456450
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
457451
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
458-
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
459452
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
460453
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
461454
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
@@ -867,8 +860,6 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
867860
gorm.io/driver/postgres v1.3.9 h1:lWGiVt5CijhQAg0PWB7Od1RNcBw/jS4d2cAScBcSDXg=
868861
gorm.io/driver/postgres v1.3.9/go.mod h1:qw/FeqjxmYqW5dBcYNBsnhQULIApQdk7YuuDPktVi1U=
869862
gorm.io/gorm v1.23.7/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
870-
gorm.io/gorm v1.23.8 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE=
871-
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
872863
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
873864
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
874865
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=

pkg/api/api.go

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ func (h APIHandler) RegisterRoutes() {
112112
cluster.GET("/:cluster_name/resource/:namespace/:name/poll", h.ResourcePoll) // Poll for resource objects in the cluster
113113
cluster.GET("/:cluster_name/resource/:namespace/:name/debug", h.Debugger)
114114
cluster.GET("/:cluster_name/debug/:namespace/:name", h.Debugger) // Alias
115+
cluster.GET("/:cluster_name/resource/:namespace/:name/unlock", h.UnlockTFO)
115116
cluster.GET("/:cluster_name/resource/:namespace/:name/status", h.ResourceStatusCheck)
116117
cluster.GET("/:cluster_name/status/:namespace/:name", h.ResourceStatusCheck) // Alias
117118
cluster.GET("/:cluster_name/resource/:namespace/:name/last-task-log", h.LastTaskLog)

pkg/api/get_record.go

+159-25
Original file line numberDiff line numberDiff line change
@@ -731,8 +731,22 @@ func (h APIHandler) Debugger(c *gin.Context) {
731731
return
732732
}
733733
defer conn.Close()
734+
var command []string
734735

735-
podExecReadWriter, err := New(h.clientset, clusterName, namespace, name, c, cmd)
736+
execCommand := []string{
737+
"/bin/bash",
738+
"-c",
739+
`cd $TFO_MAIN_MODULE && \
740+
export PS1="\\w\\$ " && \
741+
if [[ -n "$AWS_WEB_IDENTITY_TOKEN_FILE" ]]; then
742+
export $(irsa-tokengen);
743+
echo printf "\nAWS creds set from token file\n"
744+
fi && \
745+
printf "\nTry running 'terraform init'\n\n" && bash
746+
`,
747+
}
748+
749+
podExecReadWriter, err := New(h.clientset, clusterName, namespace, name, c, cmd, command, execCommand, false, true)
736750
if err != nil {
737751
log.Printf("Failed to connect to debug pod: %s", err)
738752
return
@@ -781,6 +795,55 @@ func (h APIHandler) Debugger(c *gin.Context) {
781795
// return
782796
}
783797

798+
func (h APIHandler) UnlockTFO(c *gin.Context) {
799+
clusterName := c.Param("cluster_name")
800+
clusterID := h.getClusterID(clusterName)
801+
if clusterID == 0 {
802+
c.JSON(http.StatusUnprocessableEntity, response(http.StatusUnprocessableEntity, fmt.Sprintf("cluster_name '%s' not found", clusterName), nil))
803+
return
804+
}
805+
name := c.Param("name")
806+
namespace := c.Param("namespace")
807+
if _, err := getResource(h.clientset, clusterName, namespace, name, c); err != nil {
808+
c.JSON(http.StatusUnprocessableEntity, response(http.StatusUnprocessableEntity, fmt.Sprintf("tf resource '%s/%s' not found", namespace, name), nil))
809+
return
810+
}
811+
812+
cmd := []string{}
813+
814+
command := []string{
815+
"/bin/bash",
816+
"-c",
817+
`cd $TFO_MAIN_MODULE && \
818+
file=$(mktemp) && \
819+
terraform plan -no-color 2>$file
820+
if [[ ! -s "$file" ]] ; then
821+
echo "\nno lock detected exiting"
822+
exit 0
823+
fi && \
824+
cat $file
825+
lock=$(grep -A1 "Lock Info" $file | grep "ID") && \
826+
lock_id=$(echo $lock | sed -n 's/.*\([0-9a-fA-F-]\{36\}\).*/\1/p') && \
827+
echo lock=$lock && \
828+
echo lock_id=$lock_id && \
829+
if [ -n "$lock_id" ]; then
830+
terraform force-unlock -force $lock_id
831+
fi && \
832+
echo "Done"`,
833+
}
834+
835+
execCommand := []string{}
836+
837+
_, err := New(h.clientset, clusterName, namespace, name, c, cmd, command, execCommand, true, false)
838+
if err != nil {
839+
c.JSON(http.StatusUnprocessableEntity, response(http.StatusUnprocessableEntity, fmt.Sprintf("tfo unlock failed: %s", err), nil))
840+
return
841+
}
842+
843+
c.JSON(http.StatusOK, response(http.StatusOK, fmt.Sprint("tfo unlocked"), nil))
844+
845+
}
846+
784847
type wsWrapper struct {
785848
*websocket.Conn
786849
}
@@ -861,7 +924,7 @@ func (t TermSizer) Next() *remotecommand.TerminalSize {
861924
// }
862925

863926
// command string, argv []string, headers map[string][]string, options ...Option
864-
func New(clientset kubernetes.Interface, clusterName, namespace, name string, c *gin.Context, cmd []string) (*PodExec, error) {
927+
func New(clientset kubernetes.Interface, clusterName, namespace, name string, c *gin.Context, cmd, command, execCommand []string, isUnlock, isGoroutine bool) (*PodExec, error) {
865928
pty, tty, err := ptylib.Open()
866929
if err != nil {
867930
log.Fatal(err)
@@ -877,12 +940,26 @@ func New(clientset kubernetes.Interface, clusterName, namespace, name string, c
877940
SizeCh: sizeCh,
878941
}
879942

880-
go func() {
881-
defer pty.Close()
882-
err := RemoteDebug(clientset, clusterName, namespace, name, tty, c, termSizer, cmd)
943+
if isGoroutine {
944+
go func() {
945+
defer pty.Close()
946+
err := RemoteDebug(clientset, clusterName, namespace, name, tty, c, termSizer, cmd, command, execCommand, isUnlock)
947+
log.Println("Pod exec exited")
948+
closeCh <- err
949+
}()
950+
} else {
951+
err = RemoteDebug(clientset, clusterName, namespace, name, tty, c, termSizer, cmd, command, execCommand, isUnlock)
883952
log.Println("Pod exec exited")
884-
closeCh <- err
885-
}()
953+
if err != nil {
954+
return &PodExec{
955+
pty: pty,
956+
reader: pty,
957+
writer: pty,
958+
termSizer: termSizer,
959+
Closer: closeCh,
960+
}, err
961+
}
962+
}
886963

887964
return &PodExec{
888965
pty: pty,
@@ -930,7 +1007,7 @@ func (p *PodExec) Close() error {
9301007

9311008
// RemoteDebug starts the debug pod and connects in a tty that will be synced thru a websocket. Anything written to
9321009
// stdout will be synced to the tty. stderr logs will show up in the api logs and not the tty.
933-
func RemoteDebug(parentClientset kubernetes.Interface, clusterName, namespace, name string, tty *os.File, c *gin.Context, terminalSizeQueue remotecommand.TerminalSizeQueue, cmd []string) error {
1010+
func RemoteDebug(parentClientset kubernetes.Interface, clusterName, namespace, name string, tty *os.File, c *gin.Context, terminalSizeQueue remotecommand.TerminalSizeQueue, cmd, command, execCommand []string, isUnlock bool) error {
9341011

9351012
config, err := getVclusterConfig(parentClientset, "internal", clusterName)
9361013
if err != nil {
@@ -948,12 +1025,21 @@ func RemoteDebug(parentClientset kubernetes.Interface, clusterName, namespace, n
9481025
if err != nil {
9491026
return err
9501027
}
1028+
pod := generatePod(tf, command)
9511029

952-
pod := generatePod(tf)
9531030
pod, err = podClient.Create(c, pod, metav1.CreateOptions{})
9541031
if err != nil {
9551032
return err
9561033
}
1034+
1035+
if isUnlock == true {
1036+
err = getPodStatus(pod, clientset, namespace)
1037+
if err != nil {
1038+
return err
1039+
}
1040+
return nil
1041+
}
1042+
9571043
defer podClient.Delete(c, pod.Name, metav1.DeleteOptions{})
9581044

9591045
fmt.Printf("Connecting to %s ", pod.Name)
@@ -1001,18 +1087,6 @@ func RemoteDebug(parentClientset kubernetes.Interface, clusterName, namespace, n
10011087
}
10021088
// log.Println(file.Name())
10031089
// log.Println("Setting up request")
1004-
execCommand := []string{
1005-
"/bin/bash",
1006-
"-c",
1007-
`cd $TFO_MAIN_MODULE && \
1008-
export PS1="\\w\\$ " && \
1009-
if [[ -n "$AWS_WEB_IDENTITY_TOKEN_FILE" ]]; then
1010-
export $(irsa-tokengen);
1011-
echo printf "\nAWS creds set from token file\n"
1012-
fi && \
1013-
printf "\nTry running 'terraform init'\n\n" && bash
1014-
`,
1015-
}
10161090

10171091
if len(cmd) > 0 {
10181092
execCommand = cmd
@@ -1061,6 +1135,62 @@ func RemoteDebug(parentClientset kubernetes.Interface, clusterName, namespace, n
10611135
return nil
10621136
}
10631137

1138+
func getPodStatus(pod *corev1.Pod, clientset *kubernetes.Clientset, namespace string) error {
1139+
podStatusStartTime := time.Now()
1140+
for {
1141+
pod, err := clientset.CoreV1().Pods(namespace).Get(context.TODO(), pod.Name, metav1.GetOptions{})
1142+
if err != nil {
1143+
return err
1144+
}
1145+
phase := pod.Status.Phase
1146+
if phase == "Succeeded" {
1147+
time.Sleep(120)
1148+
err := deletePod(namespace, pod.Name, clientset)
1149+
if err != nil {
1150+
return err
1151+
}
1152+
return nil
1153+
} else if phase == "Failed" {
1154+
time.Sleep(120)
1155+
err := deletePod(namespace, pod.Name, clientset)
1156+
if err != nil {
1157+
return err
1158+
}
1159+
return fmt.Errorf("Pod failed: phase %s", phase)
1160+
}
1161+
time.Sleep(5 * time.Second)
1162+
isDeleted, err := podTimeToLive(podStatusStartTime, pod.Name, namespace, clientset, 300)
1163+
if err != nil {
1164+
return err
1165+
}
1166+
if isDeleted == true {
1167+
return fmt.Errorf("Pod did not complete in time and was forcefully deleted")
1168+
}
1169+
1170+
}
1171+
1172+
}
1173+
1174+
func deletePod(namespace, podName string, clientset *kubernetes.Clientset) error {
1175+
err := clientset.CoreV1().Pods(namespace).Delete(context.TODO(), podName, metav1.DeleteOptions{})
1176+
if err != nil {
1177+
return err
1178+
}
1179+
return nil
1180+
}
1181+
1182+
func podTimeToLive(podStatusStartTime time.Time, podName, namespace string, clientset *kubernetes.Clientset, timeToLive time.Duration) (bool, error) {
1183+
var deleteCompleted = false
1184+
if time.Since(podStatusStartTime) >= timeToLive*time.Second {
1185+
err := deletePod(namespace, podName, clientset)
1186+
if err != nil {
1187+
return deleteCompleted, err
1188+
}
1189+
deleteCompleted = true
1190+
}
1191+
return deleteCompleted, nil
1192+
}
1193+
10641194
// func isTerminal(file *os.File) bool {
10651195

10661196
// inFd := file.Fd()
@@ -1070,7 +1200,7 @@ func RemoteDebug(parentClientset kubernetes.Interface, clusterName, namespace, n
10701200

10711201
// }
10721202

1073-
func generatePod(tf *tfv1beta1.Terraform) *corev1.Pod {
1203+
func generatePod(tf *tfv1beta1.Terraform, command []string) *corev1.Pod {
10741204
terraformVersion := tf.Spec.TerraformVersion
10751205
if terraformVersion == "" {
10761206
terraformVersion = "1.1.5"
@@ -1230,13 +1360,17 @@ func generatePod(tf *tfv1beta1.Terraform) *corev1.Pod {
12301360
}
12311361
restartPolicy := corev1.RestartPolicyNever
12321362

1363+
if command == nil {
1364+
command = []string{
1365+
"/bin/sleep", "86400",
1366+
}
1367+
}
1368+
12331369
containers = append(containers, corev1.Container{
12341370
SecurityContext: securityContext,
12351371
Name: "debug",
12361372
Image: "ghcr.io/galleybytes/terraform-operator-tftaskv1.1.0:" + terraformVersion,
1237-
Command: []string{
1238-
"/bin/sleep", "86400",
1239-
},
1373+
Command: command,
12401374
ImagePullPolicy: corev1.PullIfNotPresent,
12411375
EnvFrom: envFrom,
12421376
Env: env,

0 commit comments

Comments
 (0)