Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 36 additions & 12 deletions cmd/jobsink/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"fmt"
"log"
"net/http"
"net/url"
"strings"
"time"

Expand Down Expand Up @@ -424,18 +425,13 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

func (h *Handler) handleGet(ctx context.Context, w http.ResponseWriter, r *http.Request) {
logger := logging.FromContext(ctx)
parts := strings.Split(strings.TrimSuffix(r.RequestURI, "/"), "/")
if len(parts) != 9 {
ref, eventSource, eventID, err := parseLocation(r.RequestURI)
if err != nil {
logger.Info("Malformed uri", zap.String("URI", r.RequestURI))
w.WriteHeader(http.StatusBadRequest)
return
}

ref := types.NamespacedName{
Namespace: parts[2],
Name: parts[4],
}

js, err := h.lister.JobSinks(ref.Namespace).Get(ref.Name)
if err != nil {
logger.Warn("Failed to retrieve jobsink", zap.String("ref", ref.String()), zap.Error(err))
Expand All @@ -450,10 +446,6 @@ func (h *Handler) handleGet(ctx context.Context, w http.ResponseWriter, r *http.
logger.Warn("Failed to verify AuthN and AuthZ.", zap.Error(err))
return
}

eventSource := parts[6]
eventID := parts[8]

jobName := toJobName(ref.Name, eventSource, eventID)

job, err := h.k8s.BatchV1().Jobs(ref.Namespace).Get(r.Context(), jobName, metav1.GetOptions{})
Expand Down Expand Up @@ -499,13 +491,45 @@ func getServerTLSConfig(ctx context.Context) (*tls.Config, error) {
}

func locationHeader(ref types.NamespacedName, source, id string) string {
return fmt.Sprintf("/namespaces/%s/name/%s/sources/%s/ids/%s", ref.Namespace, ref.Name, source, id)
return fmt.Sprintf(
"/namespaces/%s/name/%s/sources/%s/ids/%s",
url.PathEscape(ref.Namespace),
url.PathEscape(ref.Name),
url.PathEscape(source),
url.PathEscape(id),
)
}

func jobLabelSelector(ref types.NamespacedName, id string) string {
return fmt.Sprintf("%s=%s,%s=%s", sinks.JobSinkIDLabel, id, sinks.JobSinkNameLabel, ref.Name)
}

func parseLocation(requestURI string) (types.NamespacedName, string, string, error) {
parts := strings.Split(strings.TrimSuffix(requestURI, "/"), "/")
if len(parts) != 9 {
return types.NamespacedName{}, "", "", fmt.Errorf("unexpected path format")
}

namespace, err := url.PathUnescape(parts[2])
if err != nil {
return types.NamespacedName{}, "", "", fmt.Errorf("invalid namespace path segment: %w", err)
}
name, err := url.PathUnescape(parts[4])
if err != nil {
return types.NamespacedName{}, "", "", fmt.Errorf("invalid name path segment: %w", err)
}
source, err := url.PathUnescape(parts[6])
if err != nil {
return types.NamespacedName{}, "", "", fmt.Errorf("invalid source path segment: %w", err)
}
id, err := url.PathUnescape(parts[8])
if err != nil {
return types.NamespacedName{}, "", "", fmt.Errorf("invalid id path segment: %w", err)
}

return types.NamespacedName{Namespace: namespace, Name: name}, source, id, nil
}

func toJobName(js string, source, id string) string {
h := md5.Sum([]byte(source + id)) //nolint:gosec
return kmeta.ChildName(js+"-", utils.ToDNS1123Subdomain(hex.EncodeToString(h[:])))
Expand Down
54 changes: 54 additions & 0 deletions cmd/jobsink/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"testing"

"k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/types"

"knative.dev/eventing/pkg/utils"
)
Expand Down Expand Up @@ -104,3 +105,56 @@ func FuzzToJobName(f *testing.F) {
}
})
}

func TestLocationHeaderRoundTrip(t *testing.T) {
testCases := map[string]struct {
ref types.NamespacedName
source string
id string
}{
"simple": {
ref: types.NamespacedName{
Namespace: "test-namespace",
Name: "job-sink",
},
source: "mysource3",
id: "2234-5678",
},
"slashes in source and id": {
ref: types.NamespacedName{
Namespace: "test-namespace",
Name: "job-sink",
},
source: "https://example.com/sources/my/source",
id: "event/id/with/slashes",
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
location := locationHeader(tc.ref, tc.source, tc.id)

gotRef, gotSource, gotID, err := parseLocation(location)
if err != nil {
t.Fatal("unexpected error:", err)
}

if gotRef != tc.ref {
t.Fatalf("unexpected ref: got %#v want %#v", gotRef, tc.ref)
}
if gotSource != tc.source {
t.Fatalf("unexpected source: got %q want %q", gotSource, tc.source)
}
if gotID != tc.id {
t.Fatalf("unexpected id: got %q want %q", gotID, tc.id)
}
})
}
}

func TestParseLocationRejectsUnescapedSourceSlashes(t *testing.T) {
_, _, _, err := parseLocation("/namespaces/test-namespace/name/job-sink/sources/https://example.com/source/ids/event-id")
if err == nil {
t.Fatal("expected error for malformed location")
}
}
Loading