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

Implement Video Recording Capabilities #51

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d0f1c26
create a video container in the pod when it's requested
Castone22 Jan 13, 2022
9f0a868
add a flag to override the video recorder image
Castone22 Jan 13, 2022
e1a377b
write tests without permutation since they're functional
Castone22 Jan 13, 2022
32b0f93
only check up to read error: open for config file existence test, as …
Castone22 Jan 13, 2022
3fe05e8
checksum file
Castone22 Jan 13, 2022
dbcc77c
add a missing parameter binding, get a little more specific with the …
Castone22 Jan 18, 2022
d0ee53f
add missing environment variable references
Castone22 Feb 4, 2022
7df4465
missed a comma
Castone22 Feb 4, 2022
d9b5968
fix various compile errors
Castone22 Feb 4, 2022
3ebe10b
expose xdisplay port (60+99) for ffmpeg
Castone22 Feb 4, 2022
39dcebd
copy paste error
Castone22 Feb 4, 2022
0b173be
default video name to session id
Castone22 Feb 4, 2022
4eec9d4
set browser container name to localhost (it must be this.)
Castone22 Feb 4, 2022
b7a7fff
use file_name instead of video_name
Castone22 Feb 4, 2022
0d24efb
terminate ffmpeg softly
Castone22 Feb 4, 2022
c0551e3
add a pre-stop hook to ensure the browser is the last thing to exit s…
LukeIGS Feb 21, 2022
8bd6b05
only append lifecycle when video recording is in use
LukeIGS Feb 21, 2022
bf70ab4
follow the new sg4 spec that selenoid expects
LukeIGS Apr 6, 2022
bc0362f
be a little more patient with kubedns; add a more meaningful error on…
LukeIGS Apr 6, 2022
749d85a
oh hey, my go linter started working again
LukeIGS Apr 8, 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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,7 @@ List of capabilities required for selenoid-ui compatibility:
| enableVNC | boolean | enables VNC support |
| name | string | name of test |
| screenResolution | string | custom screen resolution |
| enableVideo | boolean | enables Video capture |

</br>
Note: you can omit browser version in your desired capabilities, make sure you set defaultVersion property in the config file.
Expand Down
3 changes: 3 additions & 0 deletions cmd/selenosis/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func command() *cobra.Command {
service string
imagePullSecretName string
proxyImage string
videoImage string
sessionRetryCount int
limit int
browserWaitTimeout time.Duration
Expand Down Expand Up @@ -69,6 +70,7 @@ func command() *cobra.Command {
ServicePort: proxyPort,
ImagePullSecretName: imagePullSecretName,
ProxyImage: proxyImage,
VideoImage: videoImage,
})

if err != nil {
Expand Down Expand Up @@ -146,6 +148,7 @@ func command() *cobra.Command {
cmd.Flags().DurationVar(&shutdownTimeout, "graceful-shutdown-timeout", 30*time.Second, "time in seconds gracefull shutdown timeout")
cmd.Flags().StringVar(&imagePullSecretName, "image-pull-secret-name", "", "secret name to private registry")
cmd.Flags().StringVar(&proxyImage, "proxy-image", "alcounit/seleniferous:latest", "in case you use private registry replace with image from private registry")
cmd.Flags().StringVar(&videoImage, "video-image", "selenoid/video-recorder:latest-release", "the image to use for video recording when it's requested")
cmd.Flags().SortFlags = false

return cmd
Expand Down
6 changes: 3 additions & 3 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,18 @@ func TestConfigFile(t *testing.T) {

tests := map[string]struct {
data string
err error
err string
}{
"verify config file not exist": {
data: empty,
err: errors.New("failed to read config: read error: open : The system cannot find the file specified."),
err: "failed to read config: read error: open",
},
}

for name, test := range tests {
t.Logf("TC: %s", name)
_, err := NewBrowsersConfig(test.data)
assert.Equal(t, test.err, err)
assert.Contains(t, err.Error(), test.err)
}
}

Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand Down Expand Up @@ -261,7 +260,6 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
Expand Down Expand Up @@ -366,7 +364,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
Expand Down Expand Up @@ -458,7 +455,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
Expand Down
5 changes: 4 additions & 1 deletion handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ func (app *App) HandleSession(w http.ResponseWriter, r *http.Request) {
default:
}
if err != nil {
if strings.HasSuffix(err.Error(), ": no such host") {
continue
}
logger.WithField("time_elapsed", tools.TimeElapsed(start)).Errorf("session failed: %v", err)
tools.JSONError(w, "New session attempts retry count exceeded", http.StatusInternalServerError)
cancel()
Expand Down Expand Up @@ -225,7 +228,7 @@ func (app *App) HandleProxy(w http.ResponseWriter, r *http.Request) {
},
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
logger.Errorf("proxying session error: %v", err)
w.WriteHeader(http.StatusBadGateway)
tools.JSONError(w, fmt.Sprintf("proxying session error: %v", err), http.StatusBadGateway)
},
}).ServeHTTP(w, r)

Expand Down
158 changes: 112 additions & 46 deletions platform/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,24 @@ var (
label = "selenosis.app.type"
quotaName = "selenosis-pod-limit"
browserPorts = struct {
selenium, vnc intstr.IntOrString
selenium, vnc, video intstr.IntOrString
}{
selenium: intstr.FromString("4444"),
vnc: intstr.FromString("5900"),
video: intstr.FromString("6099"),
}

defaultsAnnotations = struct {
testName, browserName, browserVersion, screenResolution, enableVNC, timeZone string
testName, browserName, browserVersion, screenResolution, enableVNC, enableVideo, videoName, timeZone string
}{
testName: "testName",
browserName: "browserName",
browserVersion: "browserVersion",
screenResolution: "SCREEN_RESOLUTION",
enableVNC: "ENABLE_VNC",
enableVideo: "ENABLE_VIDEO",
timeZone: "TZ",
videoName: "FILE_NAME",
}
defaultLabels = struct {
serviceType, appType, session string
Expand All @@ -62,6 +65,7 @@ type ClientConfig struct {
ServicePort string
ImagePullSecretName string
ProxyImage string
VideoImage string
ReadinessTimeout time.Duration
IdleTimeout time.Duration
}
Expand Down Expand Up @@ -96,6 +100,7 @@ func NewClient(c ClientConfig) (Platform, error) {
svcPort: intstr.FromString(c.ServicePort),
imagePullSecretName: c.ImagePullSecretName,
proxyImage: c.ProxyImage,
videoImage: c.VideoImage,
readinessTimeout: c.ReadinessTimeout,
idleTimeout: c.IdleTimeout,
}
Expand Down Expand Up @@ -297,6 +302,7 @@ type service struct {
svcPort intstr.IntOrString
imagePullSecretName string
proxyImage string
videoImage string
readinessTimeout time.Duration
idleTimeout time.Duration
clientset kubernetes.Interface
Expand All @@ -308,6 +314,7 @@ func (cl *service) Create(layout ServiceSpec) (Service, error) {
defaultsAnnotations.browserName: layout.Template.BrowserName,
defaultsAnnotations.browserVersion: layout.Template.BrowserVersion,
defaultsAnnotations.testName: layout.RequestedCapabilities.TestName,
defaultsAnnotations.videoName: layout.RequestedCapabilities.VideoName,
}

labels := map[string]string{
Expand Down Expand Up @@ -356,6 +363,33 @@ func (cl *service) Create(layout ServiceSpec) (Service, error) {
}
}

i, b = envVar(defaultsAnnotations.enableVideo)
if layout.RequestedCapabilities.Video {
video := fmt.Sprintf("%v", layout.RequestedCapabilities.Video)
if !b {
layout.Template.Spec.EnvVars = append(layout.Template.Spec.EnvVars, apiv1.EnvVar{Name: defaultsAnnotations.enableVideo, Value: video})
} else {
layout.Template.Spec.EnvVars[i] = apiv1.EnvVar{Name: defaultsAnnotations.enableVideo, Value: video}
}
layout.Template.Spec.EnvVars = append(layout.Template.Spec.EnvVars, apiv1.EnvVar{Name: "BROWSER_CONTAINER_NAME", Value: "localhost"})
i, b = envVar(defaultsAnnotations.videoName)
videoName := fmt.Sprintf("%v", layout.RequestedCapabilities.VideoName)
if videoName == "" {
videoName = fmt.Sprintf("%v.mp4", layout.SessionID)
}
if !b {
layout.Template.Spec.EnvVars = append(layout.Template.Spec.EnvVars, apiv1.EnvVar{Name: defaultsAnnotations.videoName, Value: videoName})
} else {
layout.Template.Spec.EnvVars[i] = apiv1.EnvVar{Name: defaultsAnnotations.videoName, Value: video}
}
annontations[defaultsAnnotations.enableVideo] = video
annontations[defaultsAnnotations.videoName] = videoName
} else {
if b {
annontations[defaultsAnnotations.enableVideo] = layout.Template.Spec.EnvVars[i].Value
}
}

i, b = envVar(defaultsAnnotations.timeZone)
if layout.RequestedCapabilities.TimeZone != "" {
if !b {
Expand Down Expand Up @@ -386,50 +420,7 @@ func (cl *service) Create(layout ServiceSpec) (Service, error) {
layout.Template.Meta.Annotations["capabilities"] = string(caps)
}

pod := &apiv1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: layout.SessionID,
Labels: layout.Template.Meta.Labels,
Annotations: layout.Template.Meta.Annotations,
},
Spec: apiv1.PodSpec{
Hostname: layout.SessionID,
Subdomain: cl.svc,
Containers: []apiv1.Container{
{
Name: "browser",
Image: layout.Template.Image,
SecurityContext: &apiv1.SecurityContext{
Privileged: layout.Template.Privileged,
Capabilities: getCapabilities(layout.Template.Capabilities),
},
Env: layout.Template.Spec.EnvVars,
Ports: getBrowserPorts(),
Resources: layout.Template.Spec.Resources,
VolumeMounts: getVolumeMounts(layout.Template.Spec.VolumeMounts),
ImagePullPolicy: apiv1.PullIfNotPresent,
},
{
Name: "seleniferous",
Image: cl.proxyImage,
Ports: getSidecarPorts(cl.svcPort),
Command: []string{
"/seleniferous", "--listhen-port", cl.svcPort.StrVal, "--proxy-default-path", path.Join(layout.Template.Path, "session"), "--idle-timeout", cl.idleTimeout.String(), "--namespace", cl.ns,
},
ImagePullPolicy: apiv1.PullIfNotPresent,
},
},
Volumes: getVolumes(layout.Template.Volumes),
NodeSelector: layout.Template.Spec.NodeSelector,
HostAliases: layout.Template.Spec.HostAliases,
RestartPolicy: apiv1.RestartPolicyNever,
Affinity: &layout.Template.Spec.Affinity,
DNSConfig: &layout.Template.Spec.DNSConfig,
Tolerations: layout.Template.Spec.Tolerations,
ImagePullSecrets: getImagePullSecretList(cl.imagePullSecretName),
SecurityContext: getSecurityContext(layout.Template.RunAs),
},
}
pod := cl.BuildPod(layout)

context := context.Background()
pod, err := cl.clientset.CoreV1().Pods(cl.ns).Create(context, pod, metav1.CreateOptions{})
Expand Down Expand Up @@ -531,6 +522,75 @@ func (cl *service) Logs(ctx context.Context, name string) (io.ReadCloser, error)
return req.Stream(ctx)
}

func (cl *service) BuildPod(layout ServiceSpec) *apiv1.Pod {
pod := &apiv1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: layout.SessionID,
Labels: layout.Template.Meta.Labels,
Annotations: layout.Template.Meta.Annotations,
},
Spec: apiv1.PodSpec{
Hostname: layout.SessionID,
Subdomain: cl.svc,
Containers: []apiv1.Container{
{
Name: "browser",
Image: layout.Template.Image,
SecurityContext: &apiv1.SecurityContext{
Privileged: layout.Template.Privileged,
Capabilities: getCapabilities(layout.Template.Capabilities),
},
Env: layout.Template.Spec.EnvVars,
Ports: getBrowserPorts(),
Resources: layout.Template.Spec.Resources,
VolumeMounts: getVolumeMounts(layout.Template.Spec.VolumeMounts),
ImagePullPolicy: apiv1.PullIfNotPresent,
},
{
Name: "seleniferous",
Image: cl.proxyImage,
Ports: getSidecarPorts(cl.svcPort),
Command: []string{
"/seleniferous", "--listhen-port", cl.svcPort.StrVal, "--proxy-default-path", path.Join(layout.Template.Path, "session"), "--idle-timeout", cl.idleTimeout.String(), "--namespace", cl.ns,
},
ImagePullPolicy: apiv1.PullIfNotPresent,
},
},
Volumes: getVolumes(layout.Template.Volumes),
NodeSelector: layout.Template.Spec.NodeSelector,
HostAliases: layout.Template.Spec.HostAliases,
RestartPolicy: apiv1.RestartPolicyNever,
Affinity: &layout.Template.Spec.Affinity,
DNSConfig: &layout.Template.Spec.DNSConfig,
Tolerations: layout.Template.Spec.Tolerations,
ImagePullSecrets: getImagePullSecretList(cl.imagePullSecretName),
SecurityContext: getSecurityContext(layout.Template.RunAs),
},
}

if layout.RequestedCapabilities.Video {
videoContainer := apiv1.Container{
Name: "video",
Image: cl.videoImage,
Ports: getVideoPorts(),
Command: []string{},
Env: layout.Template.Spec.EnvVars,
VolumeMounts: getVolumeMounts(layout.Template.Spec.VolumeMounts),
ImagePullPolicy: apiv1.PullIfNotPresent,
}
lifecycle := &apiv1.Lifecycle{
PreStop: &apiv1.Handler{
Exec: &apiv1.ExecAction{
Command: []string{"sh", "-c", "sleep 5"},
},
},
}
pod.Spec.Containers[0].Lifecycle = lifecycle
pod.Spec.Containers = append(pod.Spec.Containers, videoContainer)
}
return pod
}

type quota struct {
ns string
clientset kubernetes.Interface
Expand Down Expand Up @@ -618,6 +678,7 @@ func getBrowserPorts() []apiv1.ContainerPort {

fn("vnc", browserPorts.vnc.IntValue())
fn("selenium", browserPorts.selenium.IntValue())
fn("video", browserPorts.video.IntValue())

return port
}
Expand All @@ -631,6 +692,11 @@ func getSidecarPorts(p intstr.IntOrString) []apiv1.ContainerPort {
return port
}

func getVideoPorts() []apiv1.ContainerPort {
port := []apiv1.ContainerPort{}
return port
}

func getImagePullSecretList(secret string) []apiv1.LocalObjectReference {
refList := make([]apiv1.LocalObjectReference, 0)
if secret != "" {
Expand Down
Loading